├── .editorconfig
├── .flake8
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ └── build-test-lint.yml
├── .gitignore
├── .readthedocs.yml
├── AUTHORS.rst
├── CONTRIBUTING.rst
├── HISTORY.rst
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.rst
├── Test.ipynb
├── berserk
├── __init__.py
├── clients.py
├── enums.py
├── exceptions.py
├── formats.py
├── models.py
├── session.py
├── todo.md
└── utils.py
├── codecov.yml
├── docs
├── Makefile
├── _static
│ └── css
│ │ └── custom.css
├── announcing.rst
├── api.rst
├── authors.rst
├── conf.py
├── contributing.rst
├── history.rst
├── index.rst
├── installation.rst
├── make.bat
├── readme.rst
└── usage.rst
├── pyproject.toml
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
├── test_formats.py
├── test_models.py
├── test_session.py
└── test_utils.py
└── tox.ini
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.bat]
14 | indent_style = tab
15 | end_of_line = crlf
16 |
17 | [LICENSE]
18 | insert_final_newline = false
19 |
20 | [Makefile]
21 | indent_style = tab
22 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | count = True
3 | max-complexity = 10
4 | show-source = True
5 | statistics = True
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | * berserk version:
2 | * Python version:
3 | * Operating System:
4 |
5 | ### Description
6 |
7 | Describe what you were trying to get done.
8 | Tell us what happened, what went wrong, and what you expected to happen.
9 |
10 | ### What I Did
11 |
12 | ```
13 | Paste the command(s) you ran and the output.
14 | If there was a crash, please include the traceback here.
15 | ```
16 |
--------------------------------------------------------------------------------
/.github/workflows/build-test-lint.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Build, test, lint
5 |
6 | on:
7 | - push
8 | - pull_request
9 |
10 | jobs:
11 |
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | python-version: ["3.7", "3.8", "3.9", "3.10"]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v3
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | python -m pip install tox
29 | - name: Test with tox
30 | run: tox -e py
31 | - name: "Upload coverage to Codecov"
32 | uses: "codecov/codecov-action@v2"
33 | with:
34 | fail_ci_if_error: false
35 | flake8:
36 | runs-on: ubuntu-latest
37 |
38 | steps:
39 | - uses: actions/checkout@v3
40 | - name: Set up Python 3.10
41 | uses: actions/setup-python@v3
42 | with:
43 | python-version: '3.10'
44 | - name: Install dependencies
45 | run: |
46 | python -m pip install --upgrade pip
47 | python -m pip install tox
48 | - name: Test with tox
49 | run: tox -e flake8
50 |
--------------------------------------------------------------------------------
/.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 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | pip-wheel-metadata/
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # dotenv
85 | .env
86 |
87 | # virtualenv
88 | .venv
89 | venv/
90 | ENV/
91 |
92 | # Spyder project settings
93 | .spyderproject
94 | .spyproject
95 |
96 | # Rope project settings
97 | .ropeproject
98 |
99 | # mkdocs documentation
100 | /site
101 |
102 | # mypy
103 | .mypy_cache/
104 |
105 | # idea
106 | .idea/
107 | .vscode/
108 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | build:
2 | image: latest
3 |
4 |
--------------------------------------------------------------------------------
/AUTHORS.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Authors
3 | =======
4 |
5 | Development Lead
6 | ================
7 |
8 | * Robert Grant
9 |
10 | Developers
11 | ==========
12 |
13 | * Robert Graham
14 |
15 | Contributors
16 | ============
17 |
18 | * Harald Klein
19 | * *your name here* :)
20 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | Contributing
4 | ============
5 |
6 | Contributions are welcome, and they are greatly appreciated! Every little bit
7 | helps, and credit will always be given.
8 |
9 | You can contribute in many ways:
10 |
11 | Types of Contributions
12 | ----------------------
13 |
14 | Report Bugs
15 | ~~~~~~~~~~~
16 |
17 | Report bugs at https://github.com/rhgrant10/berserk/issues.
18 |
19 | If you are reporting a bug, please include:
20 |
21 | * Your operating system name and version.
22 | * Any details about your local setup that might be helpful in troubleshooting.
23 | * Detailed steps to reproduce the bug.
24 |
25 | Fix Bugs
26 | ~~~~~~~~
27 |
28 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help
29 | wanted" is open to whoever wants to implement it.
30 |
31 | Implement Features
32 | ~~~~~~~~~~~~~~~~~~
33 |
34 | Look through the GitHub issues for features. Anything tagged with "enhancement"
35 | and "help wanted" is open to whoever wants to implement it.
36 |
37 | Write Documentation
38 | ~~~~~~~~~~~~~~~~~~~
39 |
40 | berserk could always use more documentation, whether as part of the
41 | official berserk docs, in docstrings, or even on the web in blog posts,
42 | articles, and such.
43 |
44 | Submit Feedback
45 | ~~~~~~~~~~~~~~~
46 |
47 | The best way to send feedback is to file an issue at https://github.com/rhgrant10/berserk/issues.
48 |
49 | If you are proposing a feature:
50 |
51 | * Explain in detail how it would work.
52 | * Keep the scope as narrow as possible, to make it easier to implement.
53 | * Remember that this is a volunteer-driven project, and that contributions
54 | are welcome :)
55 |
56 | Get Started!
57 | ------------
58 |
59 | Ready to contribute? Here's how to set up `berserk` for local development.
60 |
61 | 1. Fork the `berserk` repo on GitHub.
62 | 2. Clone your fork locally::
63 |
64 | $ git clone git@github.com:your_name_here/berserk.git
65 |
66 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
67 |
68 | $ mkvirtualenv berserk
69 | $ cd berserk/
70 | $ python setup.py develop
71 |
72 | 4. Create a branch for local development::
73 |
74 | $ git checkout -b name-of-your-bugfix-or-feature
75 |
76 | Now you can make your changes locally.
77 |
78 | 5. When you're done making changes, check that your changes pass flake8 and the
79 | tests, including testing other Python versions with tox::
80 |
81 | $ flake8 berserk tests
82 | $ python setup.py test or py.test
83 | $ tox
84 |
85 | To get flake8 and tox, just pip install them into your virtualenv.
86 |
87 | 6. Commit your changes and push your branch to GitHub::
88 |
89 | $ git add .
90 | $ git commit -m "Your detailed description of your changes."
91 | $ git push origin name-of-your-bugfix-or-feature
92 |
93 | 7. Submit a pull request through the GitHub website.
94 |
95 | Pull Request Guidelines
96 | -----------------------
97 |
98 | Before you submit a pull request, check that it meets these guidelines:
99 |
100 | 1. The pull request should include tests.
101 | 2. If the pull request adds functionality, the docs should be updated. Put
102 | your new functionality into a function with a docstring, and add the
103 | feature to the list in README.rst.
104 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check
105 | https://travis-ci.org/rhgrant10/berserk/pull_requests
106 | and make sure that the tests pass for all supported Python versions.
107 |
108 | Tips
109 | ----
110 |
111 | To run a subset of tests::
112 |
113 | $ py.test tests.test_berserk
114 |
115 |
116 | Deploying
117 | ---------
118 |
119 | A reminder for the maintainers on how to deploy.
120 | Make sure all your changes are committed (including an entry in HISTORY.rst).
121 | Then run::
122 |
123 | $ bumpversion patch # possible: major / minor / patch
124 | $ git push
125 | $ git push --tags
126 |
127 | Travis will then deploy to PyPI if tests pass.
128 |
--------------------------------------------------------------------------------
/HISTORY.rst:
--------------------------------------------------------------------------------
1 | History
2 | =======
3 |
4 | 0.10.0 (2020-04-26)
5 | -------------------
6 |
7 | * Add ``Challenge.create_ai`` for creating an AI challenge
8 | * Add ``Challenge.create_open`` for creating an open challenge
9 | * Add ``Challenge.create_with_accept`` auto-acceptance of challenges using OAuth token
10 | * Bugfix for passing initial board positions in FEN for challenges
11 | * Minor fixes for docstrings
12 |
13 | 0.9.0 (2020-04-14)
14 | ------------------
15 |
16 | * Add remaining ``Board`` endpoints: seek, handle_draw_offer, offer_draw, accept_draw, and decline_draw
17 | * Multiple doc updates/fixes
18 | * Add codecov reporting
19 |
20 | 0.8.0 (2020-03-08)
21 | ------------------
22 |
23 | * Add new ``Board`` client: stream_incoming_events, stream_game_state, make_move, post_message, abort_game, and resign_game
24 |
25 | 0.7.0 (2020-01-26)
26 | ------------------
27 |
28 | * Add simuls
29 | * Add studies export and export chapter
30 | * Add tournament results, games export, and list by creator
31 | * Add user followers, users following, rating history, and puzzle activity
32 | * Add new ``Teams`` client: join, get members, kick member, and leave
33 | * Updated documentation, including new docs for some useful utils
34 | * Fixed bugs in ``Tournaments.export_games``
35 | * Deprecated ``Users.get_by_team`` - use ``Teams.get_members`` instead
36 |
37 |
38 | 0.6.1 (2020-01-20)
39 | ------------------
40 |
41 | * Add py37 to the travis build
42 | * Update development status classifier to 4 - Beta
43 | * Fix py36 issue preventing successful build
44 | * Make updates to the Makefile
45 |
46 |
47 | 0.6.0 (2020-01-20)
48 | ------------------
49 |
50 | * Add logging to the ``berserk.session`` module
51 | * Fix exception message when no cause
52 | * Fix bug in ``Broadcasts.push_pgn_update``
53 | * Update documentation and tweak the theme
54 |
55 |
56 | 0.5.0 (2020-01-20)
57 | ------------------
58 |
59 | * Add ``ResponseError`` for 4xx and 5xx responses with status code, reason, and cause
60 | * Add ``ApiError`` for all other request errors
61 | * Fix test case broken by 0.4.0 release
62 | * Put all utils code under test
63 |
64 |
65 | 0.4.0 (2020-01-19)
66 | ------------------
67 |
68 | * Add support for the broadcast endpoints
69 | * Add a utility for easily converting API objects into update params
70 | * Fix multiple bugs with the tournament create endpoint
71 | * Improve the reusability of some conversion utilities
72 | * Improve many docstrings in the client classes
73 |
74 |
75 | 0.3.2 (2020-01-04)
76 | ------------------
77 |
78 | * Fix bug where options not passed for challenge creation
79 | * Convert requirements from pinned to sematically compatible
80 | * Bump all developer dependencies
81 | * Use pytest instead of the older py.test
82 | * Use py37 in tox
83 |
84 |
85 | 0.3.1 (2018-12-23)
86 | ------------------
87 |
88 | * Convert datetime string in tournament creation response into datetime object
89 |
90 |
91 | 0.3.0 (2018-12-23)
92 | ------------------
93 |
94 | * Convert all timestamps to datetime in all responses
95 | * Provide support for challenging other players to a game
96 |
97 |
98 | 0.2.1 (2018-12-08)
99 | ------------------
100 |
101 | * Bump requests dependency to >-2.20.0 (CVE-2018-18074)
102 |
103 |
104 | 0.2.0 (2018-12-08)
105 | ------------------
106 |
107 | * Add `position` and `start_date` params to `Tournament.create`
108 | * Add `Position` enum
109 |
110 |
111 | 0.1.2 (2018-07-14)
112 | ------------------
113 |
114 | * Fix an asine bug in the docs
115 |
116 |
117 | 0.1.1 (2018-07-14)
118 | ------------------
119 |
120 | * Added tests for session and formats modules
121 | * Fixed mispelled PgnHandler class (!)
122 | * Fixed issue with trailing whitespace when splitting multiple PGN texts
123 | * Fixed the usage overview in the README
124 | * Fixed the versions for travis-ci
125 | * Made it easier to test the `JsonHandler` class
126 | * Salted the bumpversion config to taste
127 |
128 |
129 | 0.1.0 (2018-07-10)
130 | ------------------
131 |
132 | * First release on PyPI.
133 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Python client for the lichess API
5 | Copyright (C) 2018 Robert Grant
6 |
7 | This program is free software: you can redistribute it and/or modify
8 | it under the terms of the GNU General Public License as published by
9 | the Free Software Foundation, either version 3 of the License, or
10 | (at your option) any later version.
11 |
12 | This program is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License
18 | along with this program. If not, see .
19 |
20 | Also add information on how to contact you by electronic and paper mail.
21 |
22 | You should also get your employer (if you work as a programmer) or school,
23 | if any, to sign a "copyright disclaimer" for the program, if necessary.
24 | For more information on this, and how to apply and follow the GNU GPL, see
25 | .
26 |
27 | The GNU General Public License does not permit incorporating your program
28 | into proprietary programs. If your program is a subroutine library, you
29 | may consider it more useful to permit linking proprietary applications with
30 | the library. If this is what you want to do, use the GNU Lesser General
31 | Public License instead of this License. But first, please read
32 | .
33 |
34 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS.rst
2 | include CONTRIBUTING.rst
3 | include HISTORY.rst
4 | include LICENSE
5 | include README.rst
6 |
7 | recursive-include tests *
8 | recursive-exclude * __pycache__
9 | recursive-exclude * *.py[co]
10 |
11 | include requirements*.txt
12 |
13 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 |
4 | define BROWSER_PYSCRIPT
5 | import os, webbrowser, sys
6 |
7 | try:
8 | from urllib import pathname2url
9 | except:
10 | from urllib.request import pathname2url
11 |
12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
13 | endef
14 | export BROWSER_PYSCRIPT
15 |
16 | define PRINT_HELP_PYSCRIPT
17 | import re, sys
18 |
19 | for line in sys.stdin:
20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
21 | if match:
22 | target, help = match.groups()
23 | print("%-20s %s" % (target, help))
24 | endef
25 | export PRINT_HELP_PYSCRIPT
26 |
27 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
28 |
29 | help:
30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
31 |
32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
33 |
34 | clean-build: ## remove build artifacts
35 | rm -fr build/
36 | rm -fr dist/
37 | rm -fr .eggs/
38 | find . -name '*.egg-info' -exec rm -fr {} +
39 | find . -name '*.egg' -exec rm -f {} +
40 |
41 | clean-pyc: ## remove Python file artifacts
42 | find . -name '*.pyc' -exec rm -f {} +
43 | find . -name '*.pyo' -exec rm -f {} +
44 | find . -name '*~' -exec rm -f {} +
45 | find . -name '__pycache__' -exec rm -fr {} +
46 |
47 | clean-test: ## remove test and coverage artifacts
48 | rm -fr .tox/
49 | rm -f .coverage
50 | rm -fr htmlcov/
51 | rm -fr .pytest_cache
52 |
53 | lint: ## check style with flake8
54 | flake8 berserk tests
55 |
56 | test: ## run tests quickly with the default Python
57 | pytest
58 |
59 | test-all: ## run tests on every Python version with tox
60 | tox
61 |
62 | coverage: ## check code coverage quickly with the default Python
63 | coverage run --source berserk -m pytest
64 | coverage report -m
65 | coverage html
66 | $(BROWSER) htmlcov/index.html
67 |
68 | docs: ## generate Sphinx HTML documentation, including API docs
69 | # rm -f docs/berserk.rst
70 | # rm -f docs/modules.rst
71 | # sphinx-apidoc -o docs/ berserk
72 | $(MAKE) -C docs clean
73 | $(MAKE) -C docs html
74 | $(BROWSER) docs/_build/html/index.html
75 |
76 | servedocs: docs ## compile the docs watching for changes
77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
78 |
79 | release: test-all dist ## package and upload a release
80 | twine upload dist/*
81 |
82 | dist: clean ## builds source and wheel package
83 | python setup.py sdist
84 | python setup.py bdist_wheel
85 | ls -l dist
86 |
87 | install: clean ## install the package to the active Python's site-packages
88 | pip install -r requirements_dev.txt -e .
89 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =======
2 | berserk
3 | =======
4 |
5 |
6 | .. image:: https://img.shields.io/pypi/v/berserk.svg
7 | :target: https://pypi.python.org/pypi/berserk
8 | :alt: Available on PyPI
9 |
10 | .. image:: https://img.shields.io/travis/rhgrant10/berserk.svg
11 | :target: https://travis-ci.org/rhgrant10/berserk
12 | :alt: Continuous Integration
13 |
14 | .. image:: https://codecov.io/gh/rhgrant10/berserk/branch/master/graph/badge.svg
15 | :target: https://codecov.io/gh/rhgrant10/berserk
16 | :alt: Code Coverage
17 |
18 | .. image:: https://readthedocs.org/projects/berserk/badge/?version=latest
19 | :target: https://berserk.readthedocs.io/en/latest/?badge=latest
20 | :alt: Documentation Status
21 |
22 |
23 | Python client for the `Lichess API`_ (modified).
24 |
25 | .. _Lichess API: https://lichess.org/api
26 |
27 | * Free software: GNU General Public License v3
28 | * Documentation: https://berserk.readthedocs.io.
29 |
30 |
31 | Features
32 | ========
33 |
34 | * handles JSON and PGN formats at user's discretion
35 | * token auth session
36 | * easy integration with OAuth2
37 | * automatically converts time values to datetimes
38 |
39 | Usage
40 | =====
41 |
42 | You can use any ``requests.Session``-like object as a session, including those
43 | from ``requests_oauth``. A simple token session is included, as shown below:
44 |
45 | .. code-block:: python
46 |
47 | import berserk
48 |
49 | session = berserk.TokenSession(API_TOKEN)
50 | client = berserk.Client(session=session)
51 |
52 | Most if not all of the API is available:
53 |
54 | .. code-block:: python
55 |
56 | client.account.get
57 | client.account.get_email
58 | client.account.get_preferences
59 | client.account.get_kid_mode
60 | client.account.set_kid_mode
61 | client.account.upgrade_to_bot
62 |
63 | client.users.get_puzzle_activity
64 | client.users.get_realtime_statuses
65 | client.users.get_all_top_10
66 | client.users.get_leaderboard
67 | client.users.get_public_data
68 | client.users.get_activity_feed
69 | client.users.get_by_id
70 | client.users.get_by_team
71 | client.users.get_live_streamers
72 | client.users.get_users_followed
73 | client.users.get_users_following
74 | client.users.get_rating_history
75 |
76 | client.teams.get_members
77 | client.teams.join
78 | client.teams.leave
79 | client.teams.kick_member
80 |
81 | client.games.export
82 | client.games.export_by_player
83 | client.games.export_multi
84 | client.games.get_among_players
85 | client.games.get_ongoing
86 | client.games.get_tv_channels
87 |
88 | client.challenges.create
89 | client.challenges.create_ai
90 | client.challenges.create_open
91 | client.challenges.create_with_accept
92 | client.challenges.accept
93 | client.challenges.decline
94 |
95 | client.board.stream_incoming_events
96 | client.board.seek
97 | client.board.stream_game_state
98 | client.board.make_move
99 | client.board.post_message
100 | client.board.abort_game
101 | client.board.resign_game
102 | client.board.handle_draw_offer
103 | client.board.offer_draw
104 | client.board.accept_draw
105 | client.board.decline_draw
106 |
107 | client.bots.stream_incoming_events
108 | client.bots.stream_game_state
109 | client.bots.make_move
110 | client.bots.post_message
111 | client.bots.abort_game
112 | client.bots.resign_game
113 | client.bots.accept_challenge
114 | client.bots.decline_challenge
115 |
116 | client.tournaments.get
117 | client.tournaments.create
118 | client.tournaments.export_games
119 | client.tournaments.stream_results
120 | client.tournaments.stream_by_creator
121 |
122 | client.broadcasts.create
123 | client.broadcasts.get
124 | client.broadcasts.update
125 | client.broadcasts.push_pgn_update
126 |
127 | client.simuls.get
128 |
129 | client.studies.export_chapter
130 | client.studies.export
131 |
132 |
133 | Details for each function can be found in the `full documentation `_.
134 |
135 |
136 | Credits
137 | =======
138 |
139 | This package was created with Cookiecutter_ and the
140 | `audreyr/cookiecutter-pypackage`_ project template.
141 |
142 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter
143 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
144 |
--------------------------------------------------------------------------------
/Test.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "id": "canadian-savage",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import berserk"
11 | ]
12 | },
13 | {
14 | "cell_type": "code",
15 | "execution_count": 2,
16 | "id": "worldwide-talent",
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "\n",
21 | "session = berserk.TokenSession(\"lip_WemfGeUdnPbJTOoKndeV\")\n",
22 | "client = berserk.Client(session=session)\n"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": 3,
28 | "id": "deadly-wagner",
29 | "metadata": {},
30 | "outputs": [
31 | {
32 | "data": {
33 | "text/plain": [
34 | "'qLV4LUK4'"
35 | ]
36 | },
37 | "execution_count": 3,
38 | "metadata": {},
39 | "output_type": "execute_result"
40 | }
41 | ],
42 | "source": [
43 | "client.games.import_game(\"\"\"[Event \"🕸️ Opening Traps 🕸️: Fishing Pole Trap\"]\n",
44 | "[Site \"https://lichess.org/study/Of3mcPk8/NHPhHyxK\"]\n",
45 | "[Date \"????.??.??\"]\n",
46 | "[Result \"*\"]\n",
47 | "[FEN \"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"]\n",
48 | "[UTCDate \"2016.07.21\"]\n",
49 | "[UTCTime \"14:05:49\"]\n",
50 | "[Variant \"Standard\"]\n",
51 | "[ECO \"C65\"]\n",
52 | "[Opening \"Ruy Lopez: Berlin Defense, Fishing Pole Variation\"]\n",
53 | "[Annotator \"https://lichess.org/@/Toxenory\"]\n",
54 | "\n",
55 | "1. e4 e5 2. Nf3 Nc6 3. Bb5 Nf6 4. O-O Ng4 5. h3 h5 6. hxg4 hxg4 7. Ne1 Qh4 8. f3 g3 { [%csl Gf2][%cal Gg3f2] } 9. Bxc6 Qh1# *\n",
56 | "\n",
57 | "\n",
58 | "\"\"\")"
59 | ]
60 | }
61 | ],
62 | "metadata": {
63 | "kernelspec": {
64 | "display_name": "Python 3 (ipykernel)",
65 | "language": "python",
66 | "name": "python3"
67 | },
68 | "language_info": {
69 | "codemirror_mode": {
70 | "name": "ipython",
71 | "version": 3
72 | },
73 | "file_extension": ".py",
74 | "mimetype": "text/x-python",
75 | "name": "python",
76 | "nbconvert_exporter": "python",
77 | "pygments_lexer": "ipython3",
78 | "version": "3.9.2"
79 | }
80 | },
81 | "nbformat": 4,
82 | "nbformat_minor": 5
83 | }
84 |
--------------------------------------------------------------------------------
/berserk/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Top-level package for berserk."""
3 |
4 |
5 | __author__ = """Robert Grant"""
6 | __email__ = 'rhgrant10@gmail.com'
7 | __version__ = '0.10.0'
8 |
9 |
10 | from .clients import Client # noqa: F401
11 | from .enums import Color # noqa: F401
12 | from .enums import Mode # noqa: F401
13 | from .enums import PerfType # noqa: F401
14 | from .enums import Position # noqa: F401
15 | from .enums import Room # noqa: F401
16 | from .enums import Variant # noqa: F401
17 | from .formats import JSON # noqa: F401
18 | from .formats import LIJSON # noqa: F401
19 | from .formats import NDJSON # noqa: F401
20 | from .formats import PGN # noqa: F401
21 | from .session import Requestor # noqa: F401
22 | from .session import TokenSession # noqa: F401
23 |
--------------------------------------------------------------------------------
/berserk/clients.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from time import time as now
3 |
4 | import requests
5 | from deprecated import deprecated
6 |
7 | from . import models
8 | from .formats import (
9 | JSON,
10 | LIJSON,
11 | NDJSON,
12 | PGN,
13 | TEXT,
14 | )
15 | from .session import Requestor
16 |
17 | __all__ = [
18 | 'Client',
19 | 'Account',
20 | 'Board',
21 | 'Bots',
22 | 'Broadcasts',
23 | 'Challenges',
24 | 'Games',
25 | 'Simuls',
26 | 'Studies',
27 | 'Teams',
28 | 'Tournaments',
29 | 'Users',
30 | 'TV',
31 | 'Puzzles',
32 | 'OpeningExplorer',
33 | ]
34 |
35 |
36 | # Base URL for the API
37 | API_URL = 'https://lichess.org/'
38 |
39 |
40 | class BaseClient:
41 | def __init__(self, session, base_url=None):
42 | self._r = Requestor(session, base_url or API_URL, default_fmt=JSON)
43 |
44 |
45 | class FmtClient(BaseClient):
46 | """Client that can return PGN or not.
47 |
48 | :param session: request session, authenticated as needed
49 | :type session: :class:`requests.Session`
50 | :param str base_url: base URL for the API
51 | :param bool pgn_as_default: ``True`` if PGN should be the default format
52 | for game exports when possible. This defaults
53 | to ``False`` and is used as a fallback when
54 | ``as_pgn`` is left as ``None`` for methods that
55 | support it.
56 | """
57 |
58 | def __init__(self, session, base_url=None, pgn_as_default=False):
59 | super().__init__(session, base_url)
60 | self.pgn_as_default = pgn_as_default
61 |
62 | def _use_pgn(self, as_pgn=None):
63 | # helper to merge default with provided arg
64 | return as_pgn if as_pgn is not None else self.pgn_as_default
65 |
66 |
67 | class Client(BaseClient):
68 | """Main touchpoint for the API.
69 |
70 | All endpoints are namespaced into the clients below:
71 |
72 | - :class:`account ` - managing account information
73 | - :class:`bots ` - performing bot operations
74 | - :class:`broadcasts ` - getting and creating
75 | broadcasts
76 | - :class:`challenges ` - using challenges
77 | - :class:`games ` - getting and exporting games
78 | - :class:`simuls ` - getting simultaneous
79 | exhibition games
80 | - :class:`studies ` - exporting studies
81 | - :class:`teams ` - getting information about teams
82 | - :class:`tournaments ` - getting and
83 | creating tournaments
84 | - :class:`users ` - getting information about users
85 | - :class:`TV ` - getting information about lichess tv
86 |
87 | :param session: request session, authenticated as needed
88 | :type session: :class:`requests.Session`
89 | :param str base_url: base API URL to use (if other than the default)
90 | :param bool pgn_as_default: ``True`` if PGN should be the default format
91 | for game exports when possible. This defaults
92 | to ``False`` and is used as a fallback when
93 | ``as_pgn`` is left as ``None`` for methods that
94 | support it.
95 | """
96 |
97 | def __init__(self, session=None, base_url=None, pgn_as_default=False):
98 | session = session or requests.Session()
99 | super().__init__(session, base_url)
100 | self.account = Account(session, base_url)
101 | self.users = Users(session, base_url)
102 | self.relations = Relations(session, base_url)
103 | self.teams = Teams(session, base_url)
104 | self.games = Games(session, base_url, pgn_as_default=pgn_as_default)
105 | self.challenges = Challenges(session, base_url)
106 | self.board = Board(session, base_url)
107 | self.bots = Bots(session, base_url)
108 | self.tournaments = Tournaments(
109 | session, base_url, pgn_as_default=pgn_as_default
110 | )
111 | self.broadcasts = Broadcasts(session, base_url)
112 | self.simuls = Simuls(session, base_url)
113 | self.studies = Studies(session, base_url)
114 | self.tv = TV(session, base_url)
115 | self.puzzles = Puzzles(session, base_url)
116 | self.opening_explorer = OpeningExplorer(
117 | session, 'https://explorer.lichess.ovh/'
118 | )
119 |
120 |
121 | class Account(BaseClient):
122 | """Client for account-related endpoints."""
123 |
124 | def get(self):
125 | """Get your public information.
126 |
127 | :return: public information about the authenticated user
128 | :rtype: dict
129 | """
130 | path = 'api/account'
131 | return self._r.get(path, converter=models.Account.convert)
132 |
133 | def get_email(self):
134 | """Get your email address.
135 |
136 | :return: email address of the authenticated user
137 | :rtype: str
138 | """
139 | path = 'api/account/email'
140 | return self._r.get(path)['email']
141 |
142 | def get_preferences(self):
143 | """Get your account preferences.
144 |
145 | :return: preferences of the authenticated user
146 | :rtype: dict
147 | """
148 | path = 'api/account/preferences'
149 | return self._r.get(path)['prefs']
150 |
151 | def get_kid_mode(self):
152 | """Get your kid mode status.
153 |
154 | :return: current kid mode status
155 | :rtype: bool
156 | """
157 | path = 'api/account/kid'
158 | return self._r.get(path)['kid']
159 |
160 | def set_kid_mode(self, value):
161 | """Set your kid mode status.
162 |
163 | :param bool value: whether to enable or disable kid mode
164 | :return: success
165 | :rtype: bool
166 | """
167 | path = 'api/account/kid'
168 | params = {'v': value}
169 | return self._r.post(path, params=params)['ok']
170 |
171 | @deprecated(version='0.11.0', reason='use Bots.upgrade_to_bot instead')
172 | def upgrade_to_bot(self):
173 | """Upgrade your account to a bot account.
174 |
175 | Requires bot:play oauth scope. User cannot have any previously played
176 | games.
177 |
178 | :return: success
179 | :rtype: bool
180 | """
181 | path = 'api/bot/account/upgrade'
182 | return self._r.post(path)['ok']
183 |
184 |
185 | class Users(BaseClient):
186 | """Client for user-related endpoints."""
187 |
188 | def get_puzzle_activity(self, max=None):
189 | """Stream puzzle activity history starting with the most recent.
190 |
191 | :param int max: maximum number of entries to stream
192 | :return: puzzle activity history
193 | :rtype: iter
194 | """
195 | path = 'api/user/puzzle-activity'
196 | params = {'max': max}
197 | return self._r.get(
198 | path,
199 | params=params,
200 | fmt=NDJSON,
201 | stream=True,
202 | converter=models.PuzzleActivity.convert,
203 | )
204 |
205 | def get_realtime_statuses(self, *user_ids):
206 | """Get the online, playing, and streaming statuses of players.
207 |
208 | Only id and name fields are returned for offline users.
209 |
210 | :param user_ids: one or more user IDs (names)
211 | :return: statuses of given players
212 | :rtype: list
213 | """
214 | path = 'api/users/status'
215 | params = {'ids': ','.join(user_ids)}
216 | return self._r.get(path, params=params)
217 |
218 | def get_all_top_10(self):
219 | """Get the top 10 players for each speed and variant.
220 |
221 | :return: top 10 players in each speed and variant
222 | :rtype: dict
223 | """
224 | path = 'player'
225 | return self._r.get(path, fmt=LIJSON)
226 |
227 | def get_leaderboard(self, perf_type, count=10):
228 | """Get the leaderboard for one speed or variant.
229 |
230 | :param perf_type: speed or variant
231 | :type perf_type: :class:`~berserk.enums.PerfType`
232 | :param int count: number of players to get
233 | :return: top players for one speed or variant
234 | :rtype: list
235 | """
236 | path = f'player/top/{count}/{perf_type}'
237 | return self._r.get(path, fmt=LIJSON)['users']
238 |
239 | def get_public_data(self, username):
240 | """Get the public data for a user.
241 |
242 | :param str username: username
243 | :return: public data available for the given user
244 | :rtype: dict
245 | """
246 | path = f'api/user/{username}'
247 | return self._r.get(path, converter=models.User.convert)
248 |
249 | def get_activity_feed(self, username):
250 | """Get the activity feed of a user.
251 |
252 | :param str username: username
253 | :return: activity feed of the given user
254 | :rtype: list
255 | """
256 | path = f'api/user/{username}/activity'
257 | return self._r.get(path, converter=models.Activity.convert)
258 |
259 | def get_by_id(self, *usernames):
260 | """Get multiple users by their IDs.
261 |
262 | :param usernames: one or more usernames
263 | :return: user data for the given usernames
264 | :rtype: list
265 | """
266 | path = 'api/users'
267 | return self._r.post(
268 | path, data=','.join(usernames), converter=models.User.convert
269 | )
270 |
271 | @deprecated(version='0.7.0', reason='use Teams.get_members(id) instead')
272 | def get_by_team(self, team_id):
273 | """Get members of a team.
274 |
275 | :param str team_id: ID of a team
276 | :return: users on the given team
277 | :rtype: iter
278 | """
279 | path = f'team/{team_id}/users'
280 | return self._r.get(
281 | path, fmt=NDJSON, stream=True, converter=models.User.convert
282 | )
283 |
284 | def get_live_streamers(self):
285 | """Get basic information about currently streaming users.
286 |
287 | :return: users currently streaming a game
288 | :rtype: list
289 | """
290 | path = 'streamer/live'
291 | return self._r.get(path)
292 |
293 | @deprecated(version='0.11.0', reason='moved to Relations')
294 | def get_users_followed(self, username):
295 | """Stream users followed by a user.
296 |
297 | :param str username: a username
298 | :return: iterator over the users the given user follows
299 | :rtype: iter
300 | """
301 | path = f'/api/user/{username}/following'
302 | return self._r.get(
303 | path, stream=True, fmt=NDJSON, converter=models.User.convert
304 | )
305 |
306 | @deprecated(version='0.11.0', reason='moved to relations and removed')
307 | def get_users_following(self, username):
308 | """Stream users who follow a user.
309 |
310 | :param str username: a username
311 | :return: iterator over the users that follow the given user
312 | :rtype: iter
313 | """
314 | path = f'/api/user/{username}/followers'
315 | return self._r.get(
316 | path, stream=True, fmt=NDJSON, converter=models.User.convert
317 | )
318 |
319 | def get_rating_history(self, username):
320 | """Get the rating history of a user.
321 |
322 | :param str username: a username
323 | :return: rating history for all game types
324 | :rtype: list
325 | """
326 | path = f'/api/user/{username}/rating-history'
327 | return self._r.get(path, converter=models.RatingHistory.convert)
328 |
329 | def get_performance_statistics(self, username, perf):
330 | """Get the performance statistics of a user.
331 |
332 | :param str username: a username
333 | :param str perf: a perf (https://lichess.org/api#operation/apiUserPerf)
334 | :return: performance history of username in perf
335 | :rtype: list
336 | """
337 | path = f'/api/user/{username}/perf/{perf}'
338 | return self._r.get(path, converter=models.User.convert)
339 |
340 | def get_crosstable(self, user1, user2, matchup=False):
341 | """Get the number of games and current score of any two users.
342 |
343 | Matchup information is only returned if the two given users are
344 | playing at the moment of the request.
345 |
346 | :param str user1: user1
347 | :param str user2: user2
348 | :param bool matchup: whether to include current match data
349 | :return: total number of games and current score of the given users
350 | :rtype: dict
351 | """
352 | path = f'/api/crosstable/{user1}/{user2}'
353 | params = {'matchup': str(bool(matchup)).lower()}
354 | return self._r.get(path, params=params)
355 |
356 |
357 | class Relations(BaseClient):
358 | def get_users_followed(self, username):
359 | """Stream users followed by a user.
360 |
361 | :param str username: a username
362 | :return: iterator over the users the given user follows
363 | :rtype: iter
364 | """
365 | path = f'/api/user/{username}/following'
366 | return self._r.get(
367 | path, stream=True, fmt=NDJSON, converter=models.User.convert
368 | )
369 |
370 | @deprecated(version='0.11.0', reason='Removed from Lichess API.')
371 | def get_users_following(self, username):
372 | """Stream users who follow a user.
373 |
374 | :param str username: a username
375 | :return: iterator over the users that follow the given user
376 | :rtype: iter
377 | """
378 | path = f'/api/user/{username}/followers'
379 | return self._r.get(
380 | path, stream=True, fmt=NDJSON, converter=models.User.convert
381 | )
382 |
383 | def follow_user(self, username):
384 | """Follow a user.
385 |
386 | :param str username: username of player to follow.
387 | :return: success
388 | :rtype: bool
389 | """
390 | path = f'/api/rel/follow/{username}'
391 | return self._r.post(path)['ok']
392 |
393 | def unfollow_user(self, username):
394 | """Unfollow a user.
395 |
396 | :param str username: username of player to unfollow.
397 | :return: success
398 | :rtype: bool
399 | """
400 | path = f'/api/rel/unfollow/{username}'
401 | return self._r.post(path)['ok']
402 |
403 |
404 | class Teams(BaseClient):
405 | def get_swiss_tournaments(
406 | self, team_id, max_tournaments=100, stream=False
407 | ):
408 | """Get swiss tournaments of a team.
409 |
410 | :param str team_id: ID of a team
411 | :param int max_tournaments: how many entries to download
412 | :param bool stream: whether to stream data or not
413 | :return: swiss tournaments of the given team
414 | :rtype: list or iter
415 | """
416 | path = f'api/team/{team_id}/swiss'
417 | params = {'max': max_tournaments}
418 | return self._r.get(
419 | path, params=params, converter=models.Tournament.convert,
420 | stream=stream, fmt=NDJSON,
421 | )
422 |
423 | def get_team(self, team_id):
424 | """Get a single team informations.
425 |
426 | :param str team_id: ID of a team
427 | :return: informations about the given team
428 | :rtype: dict
429 | """
430 | path = f'api/team/{team_id}'
431 | return self._r.get(path)
432 |
433 | def get_popular(self, page=1):
434 | """Get popular teams, page by page.
435 |
436 | :param int page: page to get
437 | :return: popular teams infos
438 | :rtype: dict
439 | """
440 | path = 'api/team/all'
441 | params = {'page': page}
442 | return self._r.get(path, params=params)
443 |
444 | def get_player_teams(self, username):
445 | """Get teams of a player.
446 |
447 | :param str username: a username
448 | :return: teams of the given user
449 | :rtype: list
450 | """
451 | path = f'api/team/of/{username}'
452 | return self._r.get(path)
453 |
454 | def search_teams(self, text, page=1):
455 | """Search teams by keyword.
456 |
457 | :param str text: search keyword
458 | :param int page: page to get
459 | :return: search results
460 | :rtype: list
461 | """
462 | path = 'api/team/search'
463 | params = {'text': text, 'page': page}
464 | return self._r.get(path, params=params)
465 |
466 | def get_members(self, team_id):
467 | """Get members of a team.
468 |
469 | :param str team_id: ID of a team
470 | :return: users on the given team
471 | :rtype: iter
472 | """
473 | path = f'api/team/{team_id}/users'
474 | return self._r.get(
475 | path, fmt=NDJSON, stream=True, converter=models.User.convert
476 | )
477 |
478 | def get_arena_tournaments(
479 | self, team_id, max_tournaments=100, stream=False
480 | ):
481 | """Get Arena tournaments of a team.
482 |
483 | :param str team_id: ID of a team
484 | :param int max_tournaments: how many entries to download
485 | :param bool stream: whether to stream data or not
486 | :return: arena tournaments of the given team
487 | :rtype: list or iter
488 | """
489 | path = f'api/team/{team_id}/arena'
490 | params = {'max': max_tournaments}
491 | return self._r.get(
492 | path, params=params, converter=models.Tournaments.convert,
493 | stream=stream, fmt=NDJSON,
494 | )
495 |
496 | def join(self, team_id, message=None, password=None):
497 | """Join a team.
498 |
499 | :param str team_id: ID of a team
500 | :param str message: optional request message, if the team requires one
501 | :param str password: optional password, if the team requires one
502 | :return: success
503 | :rtype: bool
504 | """
505 | path = f'team/{team_id}/join'
506 | payload = {
507 | 'message': message,
508 | 'password': password,
509 | }
510 | return self._r.post(path, data=payload)['ok']
511 |
512 | def leave(self, team_id):
513 | """Leave a team.
514 |
515 | :param str team_id: ID of a team
516 | :return: success
517 | :rtype: bool
518 | """
519 | path = f'team/{team_id}/quit'
520 | return self._r.post(path)['ok']
521 |
522 | def kick_member(self, team_id, user_id):
523 | """Kick a member out of your team.
524 |
525 | :param str team_id: ID of a team
526 | :param str user_id: ID of a team member
527 | :return: success
528 | :rtype: bool
529 | """
530 | path = f'team/{team_id}/kick/{user_id}'
531 | return self._r.post(path)['ok']
532 |
533 | def message_all(self, team_id, message):
534 | """Message all members of your team.
535 |
536 | :param str team_id: ID of a team
537 | :param str message: the message to send to all your team members
538 | :return: success
539 | :rtype: bool
540 | """
541 | path = f'team/{team_id}/pm-all'
542 | payload = {'message': message}
543 | return self._r.post(path, data=payload)['ok']
544 |
545 |
546 | class Games(FmtClient):
547 | """Client for games-related endpoints."""
548 |
549 | def export(
550 | self,
551 | game_id,
552 | as_pgn=None,
553 | moves=None,
554 | tags=None,
555 | clocks=None,
556 | evals=None,
557 | opening=None,
558 | literate=None,
559 | ):
560 | """Get one finished game as PGN or JSON.
561 |
562 | :param str game_id: the ID of the game to export
563 | :param bool as_pgn: whether to return the game in PGN format
564 | :param bool moves: whether to include the PGN moves
565 | :param bool tags: whether to include the PGN tags
566 | :param bool clocks: whether to include clock comments in the PGN moves
567 | :param bool evals: whether to include analysis evaluation comments in
568 | the PGN moves when available
569 | :param bool opening: whether to include the opening name
570 | :param bool literate: whether to include literate the PGN
571 | :return: exported game, as JSON or PGN
572 | """
573 | path = f'game/export/{game_id}'
574 | params = {
575 | 'moves': moves,
576 | 'tags': tags,
577 | 'clocks': clocks,
578 | 'evals': evals,
579 | 'opening': opening,
580 | 'literate': literate,
581 | }
582 | fmt = PGN if self._use_pgn(as_pgn) else JSON
583 | return self._r.get(
584 | path, params=params, fmt=fmt, converter=models.Game.convert
585 | )
586 |
587 | def export_ongoing(
588 | self,
589 | username,
590 | moves=True,
591 | pgn_in_json=False,
592 | tags=True,
593 | clocks=True,
594 | evals=True,
595 | opening=True,
596 | literate=False,
597 | players=None,
598 | ):
599 | """Get ongoing game of a player.
600 |
601 | :param str username: which player's games to return
602 | :param bool moves: whether to include the PGN moves
603 | :param bool pgn_in_json: whether to include the full PGN within the
604 | JSON response, in a ``pgn`` field
605 | :param bool tags: whether to include the PGN tags
606 | :param bool clocks: whether to include clock comments in the PGN moves,
607 | when available
608 | :param bool evals: whether to include analysis evaluation comments in
609 | the PGN, when available
610 | :param bool opening: whether to include the opening name
611 | :param bool literate: whether to insert textual annotations in the PGN
612 | about the opening, analysis variations, mistakes,
613 | and game termination
614 | :param str players: URL of a text file containing real names and
615 | ratings, to replace Lichess usernames and ratings in the PGN
616 | :return: exported game, as JSON or PGN
617 | """
618 | path = f'api/user/{username}/current-game'
619 | params = {
620 | 'moves': moves,
621 | 'pgnInJson': pgn_in_json,
622 | 'tags': tags,
623 | 'clocks': clocks,
624 | 'evals': evals,
625 | 'opening': opening,
626 | 'literate': literate,
627 | 'players': players,
628 | }
629 | fmt = PGN if self._use_pgn(not pgn_in_json) else JSON
630 | return self._r.get(
631 | path, params=params, fmt=fmt, converter=models.Game.convert
632 | )
633 |
634 | def export_by_player(
635 | self,
636 | username,
637 | as_pgn=None,
638 | since=None,
639 | until=None,
640 | max=None,
641 | vs=None,
642 | rated=None,
643 | perf_type=None,
644 | color=None,
645 | analysed=None,
646 | moves=None,
647 | tags=None,
648 | evals=None,
649 | opening=None,
650 | ):
651 | """Get games by player.
652 |
653 | :param str username: which player's games to return
654 | :param bool as_pgn: whether to return the game in PGN format
655 | :param int since: lowerbound on the game timestamp
656 | :param int until: upperbound on the game timestamp
657 | :param int max: limit the number of games returned
658 | :param str vs: filter by username of the opponent
659 | :param bool rated: filter by game mode (``True`` for rated, ``False``
660 | for casual)
661 | :param perf_type: filter by speed or variant
662 | :type perf_type: :class:`~berserk.enums.PerfType`
663 | :param color: filter by the color of the player
664 | :type color: :class:`~berserk.enums.Color`
665 | :param bool analysed: filter by analysis availability
666 | :param bool moves: whether to include the PGN moves
667 | :param bool tags: whether to include the PGN tags
668 | :param bool clocks: whether to include clock comments in the PGN moves
669 | :param bool evals: whether to include analysis evaluation comments in
670 | the PGN moves when available
671 | :param bool opening: whether to include the opening name
672 | :param bool literate: whether to include literate the PGN
673 | :return: iterator over the exported games, as JSON or PGN
674 | """
675 | path = f'api/games/user/{username}'
676 | params = {
677 | 'since': since,
678 | 'until': until,
679 | 'max': max,
680 | 'vs': vs,
681 | 'rated': rated,
682 | 'perfType': perf_type,
683 | 'color': color,
684 | 'analysed': analysed,
685 | 'moves': moves,
686 | 'tags': tags,
687 | 'evals': evals,
688 | 'opening': opening,
689 | }
690 | fmt = PGN if self._use_pgn(as_pgn) else NDJSON
691 | return self._r.get(
692 | path, params=params, fmt=fmt, converter=models.Game.convert
693 | )
694 |
695 | def export_multi(
696 | self,
697 | *game_ids,
698 | as_pgn=None,
699 | moves=None,
700 | tags=None,
701 | clocks=None,
702 | evals=None,
703 | opening=None,
704 | ):
705 | """Get multiple games by ID.
706 |
707 | :param game_ids: one or more game IDs to export
708 | :param bool as_pgn: whether to return the game in PGN format
709 | :param bool moves: whether to include the PGN moves
710 | :param bool tags: whether to include the PGN tags
711 | :param bool clocks: whether to include clock comments in the PGN moves
712 | :param bool evals: whether to include analysis evaluation comments in
713 | the PGN moves when available
714 | :param bool opening: whether to include the opening name
715 | :return: iterator over the exported games, as JSON or PGN
716 | """
717 | path = 'games/export/_ids'
718 | params = {
719 | 'moves': moves,
720 | 'tags': tags,
721 | 'clocks': clocks,
722 | 'evals': evals,
723 | 'opening': opening,
724 | }
725 | payload = ','.join(game_ids)
726 | fmt = PGN if self._use_pgn(as_pgn) else NDJSON
727 | yield from self._r.post(
728 | path,
729 | params=params,
730 | data=payload,
731 | fmt=fmt,
732 | stream=True,
733 | converter=models.Game.convert,
734 | )
735 |
736 | def get_among_players(self, *usernames):
737 | """Get the games currently being played among players.
738 |
739 | Note this will not includes games where only one player is in the given
740 | list of usernames.
741 |
742 | :param usernames: two or more usernames
743 | :return: iterator over all games played among the given players
744 | """
745 | path = 'api/stream/games-by-users'
746 | payload = ','.join(usernames)
747 | yield from self._r.post(
748 | path,
749 | data=payload,
750 | fmt=NDJSON,
751 | stream=True,
752 | converter=models.Game.convert,
753 | )
754 |
755 | # move this to Account?
756 | def get_ongoing(self, count=10):
757 | """Get your currently ongoing games.
758 |
759 | :param int count: number of games to get
760 | :return: some number of currently ongoing games
761 | :rtype: list
762 | """
763 | path = 'api/account/playing'
764 | params = {'nb': count}
765 | return self._r.get(path, params=params)['nowPlaying']
766 |
767 | @deprecated(version='0.11.0', reason='moved to tv')
768 | def get_tv_channels(self):
769 | """Get basic information about the best games being played.
770 |
771 | :return: best ongoing games in each speed and variant
772 | :rtype: dict
773 | """
774 | path = 'tv/channels'
775 | return self._r.get(path)
776 |
777 | def import_game(self, pgn):
778 | """Import one game from PGN.
779 |
780 | :param str pgn: the PGN, it can contain only one game
781 | :return: game id
782 | :rtype: str
783 | """
784 | path = 'api/import'
785 | payload = {'pgn': pgn}
786 | return self._r.post(path, data=payload)['id']
787 |
788 | def stream_moves(self, game_id):
789 | """Stream moves of a game as NDJSON.
790 |
791 | :param str id: the ID of the game to stream
792 | :return: game stream
793 | :rtype: dict
794 | """
795 | path = f'api/stream/game/{game_id}'
796 | return self._r.get(path, stream=True)
797 |
798 |
799 | class Challenges(BaseClient):
800 | def create(
801 | self,
802 | username,
803 | rated,
804 | clock_limit=None,
805 | clock_increment=None,
806 | days=None,
807 | color=None,
808 | variant=None,
809 | position=None,
810 | ):
811 | """Challenge another player to a game.
812 |
813 | :param str username: username of the player to challege
814 | :param bool rated: whether or not the game will be rated
815 | :param int clock_limit: clock initial time (in seconds)
816 | :param int clock_increment: clock increment (in seconds)
817 | :param int days: days per move (for correspondence games; omit clock)
818 | :param color: color of the accepting player
819 | :type color: :class:`~berserk.enums.Color`
820 | :param variant: game variant to use
821 | :type variant: :class:`~berserk.enums.Variant`
822 | :param position: custom intial position in FEN (variant must be
823 | standard and the game cannot be rated)
824 | :type position: str
825 | :return: challenge data
826 | :rtype: dict
827 | """
828 | path = f'api/challenge/{username}'
829 | payload = {
830 | 'rated': rated,
831 | 'clock.limit': clock_limit,
832 | 'clock.increment': clock_increment,
833 | 'days': days,
834 | 'color': color,
835 | 'variant': variant,
836 | 'fen': position,
837 | }
838 | return self._r.post(
839 | path, json=payload, converter=models.Tournament.convert
840 | )
841 |
842 | def create_with_accept(
843 | self,
844 | username,
845 | rated,
846 | token,
847 | clock_limit=None,
848 | clock_increment=None,
849 | days=None,
850 | color=None,
851 | variant=None,
852 | position=None,
853 | ):
854 | """Start a game with another player.
855 |
856 | This is just like the regular challenge create except it forces the
857 | opponent to accept. You must provide the OAuth token of the opponent
858 | and it must have the challenge:write scope.
859 |
860 | :param str username: username of the opponent
861 | :param bool rated: whether or not the game will be rated
862 | :param str token: opponent's OAuth token
863 | :param int clock_limit: clock initial time (in seconds)
864 | :param int clock_increment: clock increment (in seconds)
865 | :param int days: days per move (for correspondence games; omit clock)
866 | :param color: color of the accepting player
867 | :type color: :class:`~berserk.enums.Color`
868 | :param variant: game variant to use
869 | :type variant: :class:`~berserk.enums.Variant`
870 | :param position: custom intial position in FEN (variant must be
871 | standard and the game cannot be rated)
872 | :type position: :class:`~berserk.enums.Position`
873 | :return: game data
874 | :rtype: dict
875 | """
876 | path = f'api/challenge/{username}'
877 | payload = {
878 | 'rated': rated,
879 | 'acceptByToken': token,
880 | 'clock.limit': clock_limit,
881 | 'clock.increment': clock_increment,
882 | 'days': days,
883 | 'color': color,
884 | 'variant': variant,
885 | 'fen': position,
886 | }
887 | return self._r.post(
888 | path, json=payload, converter=models.Tournament.convert
889 | )
890 |
891 | def create_ai(
892 | self,
893 | level=8,
894 | clock_limit=None,
895 | clock_increment=None,
896 | days=None,
897 | color=None,
898 | variant=None,
899 | position=None,
900 | ):
901 | """Challenge AI to a game.
902 |
903 | :param int level: level of the AI (1 to 8)
904 | :param int clock_limit: clock initial time (in seconds)
905 | :param int clock_increment: clock increment (in seconds)
906 | :param int days: days per move (for correspondence games; omit clock)
907 | :param color: color of the accepting player
908 | :type color: :class:`~berserk.enums.Color`
909 | :param variant: game variant to use
910 | :type variant: :class:`~berserk.enums.Variant`
911 | :param position: use one of the custom initial positions (variant must
912 | be standard and cannot be rated)
913 | :type position: str
914 | :return: success indicator
915 | :rtype: bool
916 | """
917 | path = 'api/challenge/ai'
918 | payload = {
919 | 'level': level,
920 | 'clock.limit': clock_limit,
921 | 'clock.increment': clock_increment,
922 | 'days': days,
923 | 'color': color,
924 | 'variant': variant,
925 | 'fen': position,
926 | }
927 | return self._r.post(
928 | path, json=payload, converter=models.Tournament.convert
929 | )
930 |
931 | def create_open(
932 | self,
933 | clock_limit=None,
934 | clock_increment=None,
935 | variant=None,
936 | position=None,
937 | ):
938 | """Create a challenge that any two players can join.
939 |
940 | :param int clock_limit: clock initial time (in seconds)
941 | :param int clock_increment: clock increment (in seconds)
942 | :param variant: game variant to use
943 | :type variant: :class:`~berserk.enums.Variant`
944 | :param position: custom intial position in FEN (variant must be
945 | standard and the game cannot be rated)
946 | :type position: str
947 | :return: challenge data
948 | :rtype: dict
949 | """
950 | path = 'api/challenge/open'
951 | payload = {
952 | 'clock.limit': clock_limit,
953 | 'clock.increment': clock_increment,
954 | 'variant': variant,
955 | 'fen': position,
956 | }
957 | return self._r.post(
958 | path, json=payload, converter=models.Tournament.convert
959 | )
960 |
961 | def accept(self, challenge_id):
962 | """Accept an incoming challenge.
963 |
964 | :param str challenge_id: id of the challenge to accept
965 | :return: success indicator
966 | :rtype: bool
967 | """
968 | path = f'api/challenge/{challenge_id}/accept'
969 | return self._r.post(path)['ok']
970 |
971 | def decline(self, challenge_id):
972 | """Decline an incoming challenge.
973 |
974 | :param str challenge_id: id of the challenge to decline
975 | :return: success indicator
976 | :rtype: bool
977 | """
978 | path = f'api/challenge/{challenge_id}/decline'
979 | return self._r.post(path)['ok']
980 |
981 | def cancel(self, challenge_id, opponent_token=None):
982 | """Cancel a challenge you sent.
983 |
984 | A challenge that has been accepted but not yet played is aborted.
985 | If the ``opponent_token`` is set, the game can be canceled even if
986 | both players have moved.
987 |
988 | :param str challenge_id: id of the challenge to cancel
989 | :param str opponent_token: the opponent's ``challenge:write`` token
990 | :return: success indicator
991 | :rtype: bool
992 | """
993 | path = f'api/challenge/{challenge_id}/cancel'
994 | params = {'opponentToken': opponent_token}
995 | return self._r.post(path, params=params)['ok']
996 |
997 |
998 | class Board(BaseClient):
999 | """Client for physical board or external application endpoints."""
1000 |
1001 | def stream_incoming_events(self):
1002 | """Get your realtime stream of incoming events.
1003 |
1004 | :return: stream of incoming events
1005 | :rtype: iterator over the stream of events
1006 | """
1007 | path = 'api/stream/event'
1008 | yield from self._r.get(path, stream=True)
1009 |
1010 | def seek(
1011 | self,
1012 | time,
1013 | increment,
1014 | rated=False,
1015 | variant='standard',
1016 | color='random',
1017 | rating_range=None,
1018 | ):
1019 | """Create a public seek to start a game with a random opponent.
1020 |
1021 | :param int time: intial clock time in minutes
1022 | :param int increment: clock increment in minutes
1023 | :param bool rated: whether the game is rated (impacts ratings)
1024 | :param str variant: game variant to use
1025 | :param str color: color to play
1026 | :param rating_range: range of opponent ratings
1027 | :return: duration of the seek
1028 | :rtype: float
1029 | """
1030 | if isinstance(rating_range, (list, tuple)):
1031 | low, high = rating_range
1032 | rating_range = f'{low}-{high}'
1033 |
1034 | path = '/api/board/seek'
1035 | payload = {
1036 | 'rated': str(bool(rated)).lower(),
1037 | 'time': time,
1038 | 'increment': increment,
1039 | 'variant': variant,
1040 | 'color': color,
1041 | 'ratingRange': rating_range or '',
1042 | }
1043 |
1044 | # we time the seek
1045 | start = now()
1046 |
1047 | # just keep reading to keep the search going
1048 | for line in self._r.post(path, data=payload, fmt=TEXT, stream=True):
1049 | pass
1050 |
1051 | # and return the time elapsed
1052 | return now() - start
1053 |
1054 | def stream_game_state(self, game_id):
1055 | """Get the stream of events for a board game.
1056 |
1057 | :param str game_id: ID of a game
1058 | :return: iterator over game states
1059 | """
1060 | path = f'api/board/game/stream/{game_id}'
1061 | yield from self._r.get(
1062 | path, stream=True, converter=models.GameState.convert
1063 | )
1064 |
1065 | def make_move(self, game_id, move):
1066 | """Make a move in a board game.
1067 |
1068 | :param str game_id: ID of a game
1069 | :param str move: move to make
1070 | :return: success
1071 | :rtype: bool
1072 | """
1073 | path = f'api/board/game/{game_id}/move/{move}'
1074 | return self._r.post(path)['ok']
1075 |
1076 | def post_message(self, game_id, text, spectator=False):
1077 | """Post a message in a board game.
1078 |
1079 | :param str game_id: ID of a game
1080 | :param str text: text of the message
1081 | :param bool spectator: post to spectator room (else player room)
1082 | :return: success
1083 | :rtype: bool
1084 | """
1085 | path = f'api/board/game/{game_id}/chat'
1086 | room = 'spectator' if spectator else 'player'
1087 | payload = {'room': room, 'text': text}
1088 | return self._r.post(path, json=payload)['ok']
1089 |
1090 | def abort_game(self, game_id):
1091 | """Abort a board game.
1092 |
1093 | :param str game_id: ID of a game
1094 | :return: success
1095 | :rtype: bool
1096 | """
1097 | path = f'api/board/game/{game_id}/abort'
1098 | return self._r.post(path)['ok']
1099 |
1100 | def resign_game(self, game_id):
1101 | """Resign a board game.
1102 |
1103 | :param str game_id: ID of a game
1104 | :return: success
1105 | :rtype: bool
1106 | """
1107 | path = f'api/board/game/{game_id}/resign'
1108 | return self._r.post(path)['ok']
1109 |
1110 | def handle_draw_offer(self, game_id, accept):
1111 | """Create, accept, or decline a draw offer.
1112 |
1113 | To offer a draw, pass ``accept=True`` and a game ID of an in-progress
1114 | game. To response to a draw offer, pass either ``accept=True`` or
1115 | ``accept=False`` and the ID of a game in which you have recieved a
1116 | draw offer.
1117 |
1118 | Often, it's easier to call :func:`offer_draw`, :func:`accept_draw`, or
1119 | :func:`decline_draw`.
1120 |
1121 | :param str game_id: ID of an in-progress game
1122 | :param bool accept: whether to accept
1123 | :return: True if successful
1124 | :rtype: bool
1125 | """
1126 | accept = 'yes' if accept else 'no'
1127 | path = f'/api/board/game/{game_id}/draw/{accept}'
1128 | return self._r.post(path)['ok']
1129 |
1130 | def offer_draw(self, game_id):
1131 | """Offer a draw in the given game.
1132 |
1133 | :param str game_id: ID of an in-progress game
1134 | :return: True if successful
1135 | :rtype: bool
1136 | """
1137 | return self.handle_draw_offer(game_id, True)
1138 |
1139 | def accept_draw(self, game_id):
1140 | """Accept an already offered draw in the given game.
1141 |
1142 | :param str game_id: ID of an in-progress game
1143 | :return: True if successful
1144 | :rtype: bool
1145 | """
1146 | return self.handle_draw_offer(game_id, True)
1147 |
1148 | def decline_draw(self, game_id):
1149 | """Decline an already offered draw in the given game.
1150 |
1151 | :param str game_id: ID of an in-progress game
1152 | :return: True if successful
1153 | :rtype: bool
1154 | """
1155 | return self.handle_draw_offer(game_id, False)
1156 |
1157 |
1158 | class Bots(BaseClient):
1159 | """Client for bot-related endpoints."""
1160 |
1161 | def stream_incoming_events(self):
1162 | """Get your realtime stream of incoming events.
1163 |
1164 | :return: stream of incoming events
1165 | :rtype: iterator over the stream of events
1166 | """
1167 | path = 'api/stream/event'
1168 | yield from self._r.get(path, stream=True)
1169 |
1170 | def get_online(self, nb):
1171 | """Stream the online bot users, as ndjson.
1172 |
1173 | :param int nb: how many bot users to fetch
1174 | :return: list of online bots
1175 | :rtype: iter
1176 | """
1177 | path = 'api/bot/online'
1178 | params = {'nb': nb}
1179 | yield from self._r.get(
1180 | path, params=params, stream=True, converter=models.User.convert
1181 | )
1182 |
1183 | def upgrade_to_bot(self):
1184 | """Upgrade your account to a bot account.
1185 |
1186 | :return: success
1187 | :rtype: bool
1188 | """
1189 | path = 'api/bot/account/upgrade'
1190 | return self._r.post(path)['ok']
1191 |
1192 | def stream_game_state(self, game_id):
1193 | """Get the stream of events for a bot game.
1194 |
1195 | :param str game_id: ID of a game
1196 | :return: iterator over game states
1197 | """
1198 | path = f'api/bot/game/stream/{game_id}'
1199 | yield from self._r.get(
1200 | path, stream=True, converter=models.GameState.convert
1201 | )
1202 |
1203 | def make_move(self, game_id, move, offering_draw=False):
1204 | """Make a move in a bot game.
1205 |
1206 | :param str game_id: ID of a game
1207 | :param str move: move to make
1208 | :return: success
1209 | :rtype: bool
1210 | """
1211 | path = f'api/bot/game/{game_id}/move/{move}'
1212 | params = {'offeringDraw': offering_draw}
1213 | return self._r.post(path, params=params)['ok']
1214 |
1215 | def post_message(self, game_id, text, spectator=False):
1216 | """Post a message in a bot game.
1217 |
1218 | :param str game_id: ID of a game
1219 | :param str text: text of the message
1220 | :param bool spectator: post to spectator room (else player room)
1221 | :return: success
1222 | :rtype: bool
1223 | """
1224 | path = f'api/bot/game/{game_id}/chat'
1225 | room = 'spectator' if spectator else 'player'
1226 | payload = {'room': room, 'text': text}
1227 | return self._r.post(path, json=payload)['ok']
1228 |
1229 | def abort_game(self, game_id):
1230 | """Abort a bot game.
1231 |
1232 | :param str game_id: ID of a game
1233 | :return: success
1234 | :rtype: bool
1235 | """
1236 | path = f'api/bot/game/{game_id}/abort'
1237 | return self._r.post(path)['ok']
1238 |
1239 | def resign_game(self, game_id):
1240 | """Resign a bot game.
1241 |
1242 | :param str game_id: ID of a game
1243 | :return: success
1244 | :rtype: bool
1245 | """
1246 | path = f'api/bot/game/{game_id}/resign'
1247 | return self._r.post(path)['ok']
1248 |
1249 | @deprecated(
1250 | version='0.11.0', reason='use Challenges.accept_challenge instead'
1251 | )
1252 | def accept_challenge(self, challenge_id):
1253 | """Accept an incoming challenge.
1254 |
1255 | :param str challenge_id: ID of a challenge
1256 | :return: success
1257 | :rtype: bool
1258 | """
1259 | path = f'api/challenge/{challenge_id}/accept'
1260 | return self._r.post(path)['ok']
1261 |
1262 | @deprecated(
1263 | version='0.11.0', reason='use Challenges.decline_challenge instead'
1264 | )
1265 | def decline_challenge(self, challenge_id):
1266 | """Decline an incoming challenge.
1267 |
1268 | :param str challenge_id: ID of a challenge
1269 | :return: success
1270 | :rtype: bool
1271 | """
1272 | path = f'api/challenge/{challenge_id}/decline'
1273 | return self._r.post(path)['ok']
1274 |
1275 |
1276 | class Tournaments(FmtClient):
1277 | """Client for tournament-related endpoints."""
1278 |
1279 | def get(self):
1280 | """Get recently finished, ongoing, and upcoming tournaments.
1281 |
1282 | :return: current tournaments
1283 | :rtype: list
1284 | """
1285 | path = 'api/tournament'
1286 | return self._r.get(path, converter=models.Tournaments.convert_values)
1287 |
1288 | def create(
1289 | self,
1290 | clock_time,
1291 | clock_increment,
1292 | minutes,
1293 | name=None,
1294 | wait_minutes=None,
1295 | variant=None,
1296 | berserkable=None,
1297 | rated=None,
1298 | start_date=None,
1299 | position=None,
1300 | password=None,
1301 | conditions=None,
1302 | ):
1303 | """Create a new tournament.
1304 |
1305 | .. note::
1306 |
1307 | ``wait_minutes`` is always relative to now and is overriden by
1308 | ``start_time``.
1309 |
1310 | .. note::
1311 |
1312 | If ``name`` is left blank then one is automatically created.
1313 |
1314 | :param int clock_time: intial clock time in minutes
1315 | :param int clock_increment: clock increment in seconds
1316 | :param int minutes: length of the tournament in minutes
1317 | :param str name: tournament name
1318 | :param int wait_minutes: future start time in minutes
1319 | :param str start_date: when to start the tournament
1320 | :param str variant: variant to use if other than standard
1321 | :param bool rated: whether the game affects player ratings
1322 | :param str berserkable: whether players can use berserk
1323 | :param str position: custom initial position in FEN
1324 | :param str password: password (makes the tournament private)
1325 | :param dict conditions: conditions for participation
1326 | :return: created tournament info
1327 | :rtype: dict
1328 | """
1329 | path = 'api/tournament'
1330 | payload = {
1331 | 'name': name,
1332 | 'clockTime': clock_time,
1333 | 'clockIncrement': clock_increment,
1334 | 'minutes': minutes,
1335 | 'waitMinutes': wait_minutes,
1336 | 'startDate': start_date,
1337 | 'variant': variant,
1338 | 'rated': rated,
1339 | 'position': position,
1340 | 'berserkable': berserkable,
1341 | 'password': password,
1342 | **{f'conditions.{c}': v for c, v in (conditions or {}).items()},
1343 | }
1344 | return self._r.post(
1345 | path, json=payload, converter=models.Tournament.convert
1346 | )
1347 |
1348 | def export_games(
1349 | self,
1350 | id_,
1351 | as_pgn=False,
1352 | moves=None,
1353 | tags=None,
1354 | clocks=None,
1355 | evals=None,
1356 | opening=None,
1357 | ):
1358 | """Export games from a tournament.
1359 |
1360 | :param str id_: tournament ID
1361 | :param bool as_pgn: whether to return PGN instead of JSON
1362 | :param bool moves: include moves
1363 | :param bool tags: include tags
1364 | :param bool clocks: include clock comments in the PGN moves, when
1365 | available
1366 | :param bool evals: include analysis evalulation comments in the PGN
1367 | moves, when available
1368 | :param bool opening: include the opening name
1369 | :return: games
1370 | :rtype: list
1371 | """
1372 | path = f'api/tournament/{id_}/games'
1373 | params = {
1374 | 'moves': moves,
1375 | 'tags': tags,
1376 | 'clocks': clocks,
1377 | 'evals': evals,
1378 | 'opening': opening,
1379 | }
1380 | fmt = PGN if self._use_pgn(as_pgn) else NDJSON
1381 | return self._r.get(
1382 | path, params=params, fmt=fmt, converter=models.Game.convert
1383 | )
1384 |
1385 | def stream_results(self, id_, limit=None):
1386 | """Stream the results of a tournament.
1387 |
1388 | Results are the players of a tournament with their scores and
1389 | performance in rank order. Note that results for ongoing
1390 | tournaments can be inconsistent due to ranking changes.
1391 |
1392 | :param str id_: tournament ID
1393 | :param int limit: maximum number of results to stream
1394 | :return: iterator over the stream of results
1395 | :rtype: iter
1396 | """
1397 | path = f'api/tournament/{id_}/results'
1398 | params = {'nb': limit}
1399 | return self._r.get(path, params=params, stream=True)
1400 |
1401 | def stream_by_creator(self, username):
1402 | """Stream the tournaments created by a player.
1403 |
1404 | :param str username: username of the player
1405 | :return: tournaments
1406 | :rtype: iter
1407 | """
1408 | path = f'api/user/{username}/tournament/created'
1409 | return self._r.get(path, stream=True)
1410 |
1411 |
1412 | class Broadcasts(BaseClient):
1413 | """Broadcast of one or more games."""
1414 |
1415 | def create(
1416 | self,
1417 | name,
1418 | description,
1419 | sync_url=None,
1420 | markdown=None,
1421 | credit=None,
1422 | starts_at=None,
1423 | official=None,
1424 | throttle=None,
1425 | ):
1426 | """Create a new broadcast.
1427 |
1428 | .. note::
1429 |
1430 | ``sync_url`` must be publicly accessible. If not provided, you
1431 | must periodically push new PGN to update the broadcast manually.
1432 |
1433 | :param str name: name of the broadcast
1434 | :param str description: short description
1435 | :param str markdown: long description
1436 | :param str sync_url: URL by which Lichess can poll for updates
1437 | :param str credit: short text to give credit to the source provider
1438 | :param int starts_at: start time as millis
1439 | :param bool official: DO NOT USE
1440 | :param int throttle: DO NOT USE
1441 | :return: created tournament info
1442 | :rtype: dict
1443 | """
1444 | path = 'broadcast/new'
1445 | payload = {
1446 | 'name': name,
1447 | 'description': description,
1448 | 'syncUrl': sync_url,
1449 | 'markdown': markdown,
1450 | 'credit': credit,
1451 | 'startsAt': starts_at,
1452 | 'official': official,
1453 | 'throttle': throttle,
1454 | }
1455 | return self._r.post(
1456 | path, json=payload, converter=models.Broadcast.convert
1457 | )
1458 |
1459 | def get(self, broadcast_id, slug='-'):
1460 | """Get a broadcast by ID.
1461 |
1462 | :param str broadcast_id: ID of a broadcast
1463 | :param str slug: slug for SEO
1464 | :return: broadcast information
1465 | :rtype: dict
1466 | """
1467 | path = f'broadcast/{slug}/{broadcast_id}'
1468 | return self._r.get(path, converter=models.Broadcast.convert)
1469 |
1470 | def update(
1471 | self,
1472 | broadcast_id,
1473 | name,
1474 | description,
1475 | sync_url,
1476 | markdown=None,
1477 | credit=None,
1478 | starts_at=None,
1479 | official=None,
1480 | throttle=None,
1481 | slug='-',
1482 | ):
1483 | """Update an existing broadcast by ID.
1484 |
1485 | .. note::
1486 |
1487 | Provide all fields. Values in missing fields will be erased.
1488 |
1489 | :param str broadcast_id: ID of a broadcast
1490 | :param str name: name of the broadcast
1491 | :param str description: short description
1492 | :param str sync_url: URL by which Lichess can poll for updates
1493 | :param str markdown: long description
1494 | :param str credit: short text to give credit to the source provider
1495 | :param int starts_at: start time as millis
1496 | :param bool official: DO NOT USE
1497 | :param int throttle: DO NOT USE
1498 | :param str slug: slug for SEO
1499 | :return: updated broadcast information
1500 | :rtype: dict
1501 | """
1502 | path = f'broadcast/{slug}/{broadcast_id}'
1503 | payload = {
1504 | 'name': name,
1505 | 'description': description,
1506 | 'syncUrl': sync_url,
1507 | 'markdown': markdown,
1508 | 'credit': credit,
1509 | 'startsAt': starts_at,
1510 | 'official': official,
1511 | }
1512 | return self._r.post(
1513 | path, json=payload, converter=models.Broadcast.convert
1514 | )
1515 |
1516 | def push_pgn_update(self, broadcast_id, pgn_games, slug='-'):
1517 | """Manually update an existing broadcast by ID.
1518 |
1519 | :param str broadcast_id: ID of a broadcast
1520 | :param list pgn_games: one or more games in PGN format
1521 | :return: success
1522 | :rtype: bool
1523 | """
1524 | path = f'broadcast/{slug}/{broadcast_id}/push'
1525 | games = '\n\n'.join(g.strip() for g in pgn_games)
1526 | return self._r.post(path, data=games)['ok']
1527 |
1528 |
1529 | class Simuls(BaseClient):
1530 | """Simultaneous exhibitions - one vs many."""
1531 |
1532 | def get(self):
1533 | """Get recently finished, ongoing, and upcoming simuls.
1534 |
1535 | :return: current simuls
1536 | :rtype: list
1537 | """
1538 | path = 'api/simul'
1539 | return self._r.get(path)
1540 |
1541 |
1542 | class Studies(BaseClient):
1543 | """Study chess the Lichess way."""
1544 |
1545 | def export_chapter(self, study_id, chapter_id):
1546 | """Export one chapter of a study.
1547 |
1548 | :return: chapter
1549 | :rtype: PGN
1550 | """
1551 | path = f'/study/{study_id}/{chapter_id}.pgn'
1552 | return self._r.get(path, fmt=PGN)
1553 |
1554 | def export(self, study_id):
1555 | """Export all chapters of a study.
1556 |
1557 | :return: all chapters as PGN
1558 | :rtype: list
1559 | """
1560 | path = f'/study/{study_id}.pgn'
1561 | return self._r.get(path, fmt=PGN, stream=True)
1562 |
1563 |
1564 | class TV(FmtClient):
1565 | """Chess TV of Lichess."""
1566 |
1567 | def get_tv_channels(self):
1568 | """Get basic information about the best games being played.
1569 |
1570 | :return: best ongoing games in each speed and variant
1571 | :rtype: dict
1572 | """
1573 | path = 'api/tv/channels'
1574 | return self._r.get(path)
1575 |
1576 | def stream_current(self):
1577 | """Stream current TV game.
1578 |
1579 | :return: dict of positions and moves of the current TV game
1580 | :rtype: dict
1581 | """
1582 | path = 'api/tv/feed'
1583 | return self._r.get(path, fmt=NDJSON, stream=True)
1584 |
1585 | def get_best_ongoing(
1586 | self,
1587 | channel,
1588 | nb=10,
1589 | moves=True,
1590 | pgn_in_json=False,
1591 | tags=True,
1592 | clocks=False,
1593 | opening=False,
1594 | ):
1595 | """Get a list of ongoing games for a given TV channel.
1596 |
1597 | :param bool moves: whether to include the PGN moves
1598 | :param int nb: number of games to fetch
1599 | :param bool pgn_in_json: whether to include the full PGN within the
1600 | JSON response, in a ``pgn`` field
1601 | :param bool tags: whether to include the PGN tags
1602 | :param bool clocks: whether to include clock comments in the PGN moves,
1603 | when available
1604 | :param bool opening: whether to include the opening name
1605 | :return: exported game, as JSON or PGN
1606 | :rtype: str or dict
1607 | """
1608 | path = f'api/tv/{channel}'
1609 | params = {
1610 | 'nb': nb,
1611 | 'moves': moves,
1612 | 'pgnInJson': pgn_in_json,
1613 | 'tags': tags,
1614 | 'clocks': clocks,
1615 | 'opening': opening,
1616 | }
1617 | fmt = PGN if self._use_pgn(not pgn_in_json) else NDJSON
1618 | return self._r.get(
1619 | path, params=params, fmt=fmt, converter=models.Game.convert
1620 | )
1621 |
1622 |
1623 | class Puzzles(BaseClient):
1624 | """Chess puzzles."""
1625 |
1626 | def get_daily(self):
1627 | """Get the daily Lichess puzzle.
1628 |
1629 | :return: daily puzzle
1630 | :rtype: dict
1631 | """
1632 | path = 'api/puzzle/daily'
1633 | return self._r.get(path, fmt=JSON)
1634 |
1635 | def get_activity(self, max_entries=None):
1636 | """Get your puzzle activity.
1637 |
1638 | :param int max_entries: how many entries to download
1639 | :return: your puzzle activity
1640 | :rtype: dict
1641 | """
1642 | path = 'api/puzzle/activity'
1643 | params = {'max': max_entries}
1644 | return self._r.get(path, params=params, fmt=NDJSON)
1645 |
1646 | def get_dashboard(self, days=30):
1647 | """Get your puzzle dashboard.
1648 |
1649 | :param int days: how many days to look back when aggregating puzzle
1650 | results
1651 | :return: your puzzle dashboard
1652 | :rtype: dict
1653 | """
1654 | path = f'api/puzzle/dashboard/{days}'
1655 | return self._r.get(path, fmt=JSON)
1656 |
1657 | def get_storm_dashboard(self, username, days=30):
1658 | """Get storm dashboard of player.
1659 |
1660 | :param str username: a username
1661 | :param int days: how many days of history to return
1662 | :return: a player storm dashboard
1663 | :rtype: dict
1664 | """
1665 | path = f'api/storm/dashboard/{username}'
1666 | params = {'days': days}
1667 | return self._r.get(path, params=params, fmt=JSON)
1668 |
1669 |
1670 | class OpeningExplorer(BaseClient):
1671 | """Chess openings explorer."""
1672 |
1673 | def masters(
1674 | self,
1675 | fen=None,
1676 | play=None,
1677 | since=1952,
1678 | until=None,
1679 | moves=12,
1680 | top_games=15,
1681 | ):
1682 | """Get openings from the masters database.
1683 |
1684 | :param str fen: FEN of the root position
1685 | :param str play: comma separated sequence of legal moves in UCI
1686 | notation, play additional moves starting from ``fen``
1687 | :param int since: include only games from this year or later
1688 | :param int until: include only games from this year or earlier
1689 | :param int moves: number of most common moves to display
1690 | :param int top_games: number of top games to display
1691 | :return: masters database search results
1692 | :rtype: dict
1693 | """
1694 | path = 'masters'
1695 | params = {
1696 | 'fen': fen,
1697 | 'play': play,
1698 | 'since': since,
1699 | 'until': until,
1700 | 'moves': moves,
1701 | 'topGames': top_games,
1702 | }
1703 | return self._r.get(path, params=params)
1704 |
1705 | def lichess(
1706 | self,
1707 | variant='standard',
1708 | fen=None,
1709 | play=None,
1710 | speeds=None,
1711 | ratings=None,
1712 | since='0000-01',
1713 | until=None,
1714 | moves=12,
1715 | top_games=15,
1716 | recent_games=4,
1717 | ):
1718 | """Get openings sampled from all lichess games.
1719 |
1720 | :param str variant: variant
1721 | :param str fen: FEN of the root position
1722 | :param str play: comma separated sequence of legal moves in UCI
1723 | notation, play additional moves starting from ``fen``
1724 | :param str speeds: comma separated list of game speeds to look for
1725 | :param str ratings: comma separated list of rating groups,
1726 | ranging from their value to the next higher group
1727 | (1600, 1800, 2000, 2200, 2500)
1728 | :param int since: include only games from this month or later
1729 | :param int until: include only games from this month or earlier
1730 | :param int moves: number of most common moves to display
1731 | :param int top_games: number of top games to display
1732 | :param int recent_games: number of recent games to display
1733 | :return: lichess database search results
1734 | :rtype: dict
1735 | """
1736 | path = 'lichess'
1737 | params = {
1738 | 'variant': variant,
1739 | 'fen': fen,
1740 | 'play': play,
1741 | 'speeds': speeds,
1742 | 'ratings': ratings,
1743 | 'since': since,
1744 | 'until': until,
1745 | 'moves': moves,
1746 | 'topGames': top_games,
1747 | 'recentGames': recent_games,
1748 | }
1749 | return self._r.get(path, params=params)
1750 |
1751 | def player(
1752 | self,
1753 | player,
1754 | color,
1755 | variant='standard',
1756 | fen=None,
1757 | play=None,
1758 | speeds=None,
1759 | modes=None,
1760 | since='0000-01',
1761 | until=None,
1762 | moves=12,
1763 | recent_games=4,
1764 | ):
1765 | """Get openings of a particular lichess player.
1766 |
1767 | :param str player: a username
1768 | :param str color: white or black
1769 | :param str variant: variant
1770 | :param str fen: FEN of the root position
1771 | :param str play: comma separated sequence of legal moves in UCI
1772 | notation, play additional moves starting from ``fen``
1773 | :param str speeds: comma separated list of game speeds to look for
1774 | :param str modes: casual or rated
1775 | :param int since: include only games from this month or later
1776 | :param int until: include only games from this month or earlier
1777 | :param int moves: number of most common moves to display
1778 | :param int recent_games: number of recent games to display
1779 | :return: player database search results
1780 | :rtype: dict
1781 | """
1782 | path = 'player'
1783 | params = {
1784 | 'player': player,
1785 | 'color': color,
1786 | 'variant': variant,
1787 | 'fen': fen,
1788 | 'play': play,
1789 | 'speeds': speeds,
1790 | 'modes': modes,
1791 | 'since': since,
1792 | 'until': until,
1793 | 'moves': moves,
1794 | 'recentGames': recent_games,
1795 | }
1796 | return self._r.get(path, params=params, fmt=NDJSON)
1797 |
--------------------------------------------------------------------------------
/berserk/enums.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | __all__ = ['PerfType', 'Variant', 'Color', 'Room', 'Mode', 'Position']
5 |
6 |
7 | class GameType:
8 | ANTICHESS = 'antichess'
9 | ATOMIC = 'atomic'
10 | CHESS960 = 'chess960'
11 | CRAZYHOUSE = 'crazyhouse'
12 | HORDE = 'horde'
13 | KING_OF_THE_HILL = 'kingOfTheHill'
14 | RACING_KINGS = 'racingKings'
15 | THREE_CHECK = 'threeCheck'
16 |
17 |
18 | class PerfType(GameType):
19 | BULLET = 'bullet'
20 | BLITZ = 'blitz'
21 | RAPID = 'rapid'
22 | CLASSICAL = 'classical'
23 | ULTRA_BULLET = 'ultraBullet'
24 |
25 |
26 | class Variant(GameType):
27 | STANDARD = 'standard'
28 |
29 |
30 | class Color:
31 | WHITE = 'white'
32 | BLACK = 'black'
33 |
34 |
35 | class Room:
36 | PLAYER = 'player'
37 | SPECTATOR = 'spectator'
38 |
39 |
40 | class Mode:
41 | CASUAL = 'casual'
42 | RATED = 'rated'
43 |
44 |
45 | class Position:
46 | ALEKHINES_DEFENCE = 'rnbqkb1r/pppppppp/5n2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 2 2' # noqa: E501
47 | ALEKHINES_DEFENCE__MODERN_VARIATION = 'rnbqkb1r/ppp1pppp/3p4/3nP3/3P4/5N2/PPP2PPP/RNBQKB1R b KQkq - 1 4' # noqa: E501
48 | BENKO_GAMBIT = 'rnbqkb1r/p2ppppp/5n2/1ppP4/2P5/8/PP2PPPP/RNBQKBNR w KQkq b6 1 4' # noqa: E501
49 | BENONI_DEFENCE__CZECH_BENONI = 'rnbqkb1r/pp1p1ppp/5n2/2pPp3/2P5/8/PP2PPPP/RNBQKBNR w KQkq - 0 4' # noqa: E501
50 | BENONI_DEFENCE__MODERN_BENONI = 'rnbqkb1r/pp1p1ppp/4pn2/2pP4/2P5/8/PP2PPPP/RNBQKBNR w KQkq - 0 4' # noqa: E501
51 | BISHOPS_OPENING = 'rnbqkbnr/pppp1ppp/8/4p3/2B1P3/8/PPPP1PPP/RNBQK1NR b KQkq - 2 2' # noqa: E501
52 | BLACKMAR_DIEMER_GAMBIT = 'rnbqkbnr/ppp1pppp/8/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq e3 1 2' # noqa: E501
53 | BOGO_INDIAN_DEFENCE = 'rnbqk2r/pppp1ppp/4pn2/8/1bPP4/5N2/PP2PPPP/RNBQKB1R w KQkq - 3 4' # noqa: E501
54 | BONGCLOUD_ATTACK = 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPKPPP/RNBQ1BNR b kq - 0 2' # noqa: E501
55 | BUDAPEST_DEFENCE = 'rnbqkb1r/pppp1ppp/5n2/4p3/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3' # noqa: E501
56 | CARO_KANN_DEFENCE = 'rnbqkbnr/pp1ppppp/2p5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2' # noqa: E501
57 | CARO_KANN_DEFENCE__ADVANCE_VARIATION = 'rnbqkbnr/pp2pppp/2p5/3pP3/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 1 3' # noqa: E501
58 | CARO_KANN_DEFENCE__CLASSICAL_VARIATION = 'rn1qkbnr/pp2pppp/2p5/5b2/3PN3/8/PPP2PPP/R1BQKBNR w KQkq - 2 5' # noqa: E501
59 | CARO_KANN_DEFENCE__EXCHANGE_VARIATION = 'rnbqkbnr/pp2pppp/2p5/3P4/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 1 3' # noqa: E501
60 | CARO_KANN_DEFENCE__PANOV_BOTVINNIK_ATTACK = 'rnbqkb1r/pp3ppp/4pn2/3p4/2PP4/2N5/PP3PPP/R1BQKBNR w KQkq - 1 6' # noqa: E501
61 | CARO_KANN_DEFENCE__STEINITZ_VARIATION = 'rnbqkb1r/pp3ppp/4pn2/3p4/2PP4/2N5/PP3PPP/R1BQKBNR w KQkq - 1 6' # noqa: E501
62 | CATALAN_OPENING = 'rnbqkb1r/pppp1ppp/4pn2/8/2PP4/6P1/PP2PP1P/RNBQKBNR b KQkq - 1 3' # noqa: E501
63 | CATALAN_OPENING__CLOSED_VARIATION = 'rnbqk2r/ppp1bppp/4pn2/3p4/2PP4/5NP1/PP2PPBP/RNBQK2R b KQkq - 4 5' # noqa: E501
64 | CLOSED_GAME = 'rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 2' # noqa: E501
65 | DANISH_GAMBIT = 'rnbqkbnr/pppp1ppp/8/8/3pP3/2P5/PP3PPP/RNBQKBNR b KQkq - 1 3' # noqa: E501
66 | DUTCH_DEFENCE = 'rnbqkbnr/ppppp1pp/8/5p2/3P4/8/PPP1PPPP/RNBQKBNR w KQkq f6 1 2' # noqa: E501
67 | DUTCH_DEFENCE__LENINGRAD_VARIATION = 'rnbqk2r/ppppp1bp/5np1/5p2/2PP4/5NP1/PP2PPBP/RNBQK2R b KQkq - 4 5' # noqa: E501
68 | DUTCH_DEFENCE__STAUNTON_GAMBIT = 'rnbqkb1r/ppppp1pp/5n2/6B1/3Pp3/2N5/PPP2PPP/R2QKBNR b KQkq - 4 4' # noqa: E501
69 | DUTCH_DEFENCE__STONEWALL_VARIATION = 'rnbq1rk1/ppp1b1pp/4pn2/3p1p2/2PP4/5NP1/PP2PPBP/RNBQ1RK1 w - d6 1 7' # noqa: E501
70 | ENGLISH_OPENING = 'rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR b KQkq c3 1 1' # noqa: E501
71 | ENGLISH_OPENING__CLOSED_SYSTEM = 'r1bqk1nr/ppp2pbp/2np2p1/4p3/2P5/2NP2P1/PP2PPBP/R1BQK1NR w KQkq - 0 6' # noqa: E501
72 | ENGLISH_OPENING__REVERSED_SICILIAN = 'rnbqkbnr/pppp1ppp/8/4p3/2P5/8/PP1PPPPP/RNBQKBNR w KQkq e6 1 2' # noqa: E501
73 | ENGLISH_OPENING__SYMMETRICAL_VARIATION = 'rnbqkbnr/pp1ppppp/8/2p5/2P5/8/PP1PPPPP/RNBQKBNR w KQkq c6 1 2' # noqa: E501
74 | FOUR_KNIGHTS_GAME = 'r1bqkb1r/pppp1ppp/2n2n2/4p3/4P3/2N2N2/PPPP1PPP/R1BQKB1R w KQkq - 5 4' # noqa: E501
75 | FOUR_KNIGHTS_GAME__SCOTCH_VARIATION = 'r1bqkb1r/pppp1ppp/2n2n2/4p3/3PP3/2N2N2/PPP2PPP/R1BQKB1R b KQkq d3 1 4' # noqa: E501
76 | FOUR_KNIGHTS_GAME__SPANISH_VARIATION = 'r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/2N2N2/PPPP1PPP/R1BQK2R b KQkq - 0 4' # noqa: E501
77 | FRANKENSTEIN_DRACULA_VARIATION = 'rnbqkb1r/pppp1ppp/8/4p3/2B1n3/2N5/PPPP1PPP/R1BQK1NR w KQkq - 0 4' # noqa: E501
78 | FRENCH_DEFENCE = 'rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2' # noqa: E501
79 | FRENCH_DEFENCE__ADVANCE_VARIATION = 'rnbqkbnr/ppp2ppp/4p3/3pP3/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 1 3' # noqa: E501
80 | FRENCH_DEFENCE__BURN_VARIATION = 'rnbqkb1r/ppp2ppp/4pn2/3p2B1/3PP3/2N5/PPP2PPP/R2QKBNR b KQkq - 1 4' # noqa: E501
81 | FRENCH_DEFENCE__CLASSICAL_VARIATION = 'rnbqkb1r/ppp2ppp/4pn2/3p4/3PP3/2N5/PPP2PPP/R1BQKBNR w KQkq - 3 4' # noqa: E501
82 | FRENCH_DEFENCE__EXCHANGE_VARIATION = 'rnbqkbnr/ppp2ppp/4p3/3P4/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 1 3' # noqa: E501
83 | FRENCH_DEFENCE__RUBINSTEIN_VARIATION = 'rnbqkbnr/ppp2ppp/4p3/8/3Pp3/2N5/PPP2PPP/R1BQKBNR w KQkq - 1 4' # noqa: E501
84 | FRENCH_DEFENCE__TARRASCH_VARIATION = 'rnbqkbnr/ppp2ppp/4p3/3p4/3PP3/8/PPPN1PPP/R1BQKBNR b KQkq - 2 3' # noqa: E501
85 | FRENCH_DEFENCE__WINAWER_VARIATION = 'rnbqk1nr/ppp2ppp/4p3/3p4/1b1PP3/2N5/PPP2PPP/R1BQKBNR w KQkq - 3 4' # noqa: E501
86 | GIUOCO_PIANO = 'r1bqk1nr/pppp1ppp/2n5/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 5 4' # noqa: E501
87 | GRUNFELD_DEFENCE = 'rnbqkb1r/ppp1pp1p/5np1/3p4/2PP4/2N5/PP2PPPP/R1BQKBNR w KQkq d6 1 4' # noqa: E501
88 | GRUNFELD_DEFENCE__BRINCKMANN_ATTACK = 'rnbqkb1r/ppp1pp1p/5np1/3p4/2PP1B2/2N5/PP2PPPP/R2QKBNR b KQkq - 2 4' # noqa: E501
89 | GRUNFELD_DEFENCE__EXCHANGE_VARIATION = 'rnbqkb1r/ppp1pp1p/6p1/3n4/3P4/2N5/PP2PPPP/R1BQKBNR w KQkq - 1 5' # noqa: E501
90 | GRUNFELD_DEFENCE__RUSSIAN_VARIATION = 'rnbqkb1r/ppp1pp1p/5np1/3p4/2PP4/1QN5/PP2PPPP/R1B1KBNR b KQkq - 0 4' # noqa: E501
91 | GRUNFELD_DEFENCE__TAIMANOV_VARIATION = 'rnbqk2r/ppp1ppbp/5np1/3p2B1/2PP4/2N2N2/PP2PPPP/R2QKB1R b KQkq - 0 5' # noqa: E501
92 | HALLOWEEN_GAMBIT = 'r1bqkb1r/pppp1ppp/2n2n2/4N3/4P3/2N5/PPPP1PPP/R1BQKB1R b KQkq - 1 4' # noqa: E501
93 | HUNGARIAN_OPENING = 'rnbqkbnr/pppppppp/8/8/8/6P1/PPPPPP1P/RNBQKBNR b KQkq - 1 1' # noqa: E501
94 | ITALIAN_GAME = 'r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 4 3' # noqa: E501
95 | ITALIAN_GAME__EVANS_GAMBIT = 'r1bqk1nr/pppp1ppp/2n5/2b1p3/1PB1P3/5N2/P1PP1PPP/RNBQK2R b KQkq b3 1 4' # noqa: E501
96 | ITALIAN_GAME__HUNGARIAN_DEFENCE = 'r1bqk1nr/ppppbppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 5 4' # noqa: E501
97 | ITALIAN_GAME__TWO_KNIGHTS_DEFENCE = 'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 5 4' # noqa: E501
98 | KINGS_GAMBIT = 'rnbqkbnr/pppp1ppp/8/4p3/4PP2/8/PPPP2PP/RNBQKBNR b KQkq f3 1 2' # noqa: E501
99 | KINGS_GAMBIT_ACCEPTED = 'rnbqkbnr/pppp1ppp/8/8/4Pp2/8/PPPP2PP/RNBQKBNR w KQkq - 1 3' # noqa: E501
100 | KINGS_GAMBIT_ACCEPTED__BISHOPS_GAMBIT = 'rnbqkbnr/pppp1ppp/8/8/2B1Pp2/8/PPPP2PP/RNBQK1NR b KQkq - 2 3' # noqa: E501
101 | KINGS_GAMBIT_ACCEPTED__CLASSICAL_VARIATION = 'rnbqkbnr/pppp1p1p/8/6p1/4Pp2/5N2/PPPP2PP/RNBQKB1R w KQkq - 0 4' # noqa: E501
102 | KINGS_GAMBIT_ACCEPTED__MODERN_DEFENCE = 'rnbqkbnr/ppp2ppp/8/3p4/4Pp2/5N2/PPPP2PP/RNBQKB1R w KQkq d6 1 4' # noqa: E501
103 | KINGS_GAMBIT_DECLINED__CLASSICAL_VARIATION = 'rnbqk1nr/pppp1ppp/8/2b1p3/4PP2/8/PPPP2PP/RNBQKBNR w KQkq - 2 3' # noqa: E501
104 | KINGS_GAMBIT_DECLINED__FALKBEER_COUNTERGAMBIT = 'rnbqkbnr/ppp2ppp/8/3pp3/4PP2/8/PPPP2PP/RNBQKBNR w KQkq d6 1 3' # noqa: E501
105 | KINGS_INDIAN_ATTACK = 'rnbqkbnr/ppp1pppp/8/3p4/8/5NP1/PPPPPP1P/RNBQKB1R b KQkq - 1 2' # noqa: E501
106 | KINGS_INDIAN_DEFENCE = 'rnbqkb1r/pppppp1p/5np1/8/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 1 3' # noqa: E501
107 | KINGS_INDIAN_DEFENCE__4E4 = 'rnbqk2r/ppp1ppbp/3p1np1/8/2PPP3/2N5/PP3PPP/R1BQKBNR w KQkq - 1 5' # noqa: E501
108 | KINGS_INDIAN_DEFENCE__AVERBAKH_VARIATION = 'rnbq1rk1/ppp1ppbp/3p1np1/6B1/2PPP3/2N5/PP2BPPP/R2QK1NR b KQ - 4 6' # noqa: E501
109 | KINGS_INDIAN_DEFENCE__CLASSICAL_VARIATION = 'rnbq1rk1/ppp1ppbp/3p1np1/8/2PPP3/2N2N2/PP2BPPP/R1BQK2R b KQ - 4 6' # noqa: E501
110 | KINGS_INDIAN_DEFENCE__FIANCHETTO_VARIATION = 'rnbqk2r/ppp1ppbp/3p1np1/8/2PP4/2N2NP1/PP2PP1P/R1BQKB1R b KQkq - 1 5' # noqa: E501
111 | KINGS_INDIAN_DEFENCE__FOUR_PAWNS_ATTACK = 'rnbqk2r/ppp1ppbp/3p1np1/8/2PPPP2/2N5/PP4PP/R1BQKBNR b KQkq f3 1 5' # noqa: E501
112 | KINGS_INDIAN_DEFENCE__SAMISCH_VARIATION = 'rnbqk2r/ppp1ppbp/3p1np1/8/2PPP3/2N2P2/PP4PP/R1BQKBNR b KQkq - 1 5' # noqa: E501
113 | KINGS_PAWN = 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 1 1'
114 | LONDON_SYSTEM = 'rnbqkb1r/ppp1pppp/5n2/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R b KQkq - 4 3' # noqa: E501
115 | MODERN_DEFENCE = 'rnbqkbnr/pppppp1p/6p1/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2' # noqa: E501
116 | MODERN_DEFENCE__ROBATSCH_DEFENCE = 'rnbqk1nr/ppppppbp/6p1/8/3PP3/2N5/PPP2PPP/R1BQKBNR b KQkq - 0 3' # noqa: E501
117 | NIMZO_INDIAN_DEFENCE = 'rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 3 4' # noqa: E501
118 | NIMZO_INDIAN_DEFENCE__CLASSICAL_VARIATION = 'rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N5/PPQ1PPPP/R1B1KBNR b KQkq - 4 4' # noqa: E501
119 | NIMZO_INDIAN_DEFENCE__FISCHER_VARIATION = 'rnbqk2r/p1pp1ppp/1p2pn2/8/1bPP4/2N1P3/PP3PPP/R1BQKBNR w KQkq - 0 5' # noqa: E501
120 | NIMZO_INDIAN_DEFENCE__HUBNER_VARIATION = 'r1bqk2r/pp3ppp/2nppn2/2p5/2PP4/2PBPN2/P4PPP/R1BQK2R w KQkq - 0 8' # noqa: E501
121 | NIMZO_INDIAN_DEFENCE__KASPAROV_VARIATION = 'rnbqk2r/pppp1ppp/4pn2/8/1bPP4/2N2N2/PP2PPPP/R1BQKB1R b KQkq - 0 4' # noqa: E501
122 | NIMZO_INDIAN_DEFENCE__LENINGRAD_VARIATION = 'rnbqk2r/pppp1ppp/4pn2/6B1/1bPP4/2N5/PP2PPPP/R2QKBNR b KQkq - 0 4' # noqa: E501
123 | NIMZO_INDIAN_DEFENCE__SAMISCH_VARIATION = 'rnbqk2r/pppp1ppp/4pn2/8/2PP4/P1P5/4PPPP/R1BQKBNR b KQkq - 0 5' # noqa: E501
124 | NIMZO_LARSEN_ATTACK = 'rnbqkbnr/pppppppp/8/8/8/1P6/P1PPPPPP/RNBQKBNR b KQkq - 1 1' # noqa: E501
125 | OLD_INDIAN_DEFENCE = 'rnbqkb1r/ppp1pppp/3p1n2/8/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 1 3' # noqa: E501
126 | OPEN_GAME = 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2'
127 | PETROVS_DEFENCE = 'rnbqkb1r/pppp1ppp/5n2/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 3 3' # noqa: E501
128 | PETROVS_DEFENCE__CLASSICAL_ATTACK = 'rnbqkb1r/ppp2ppp/3p4/8/3Pn3/5N2/PPP2PPP/RNBQKB1R b KQkq d3 1 5' # noqa: E501
129 | PETROVS_DEFENCE__STEINITZ_ATTACK = 'rnbqkb1r/pppp1ppp/5n2/4p3/3PP3/5N2/PPP2PPP/RNBQKB1R b KQkq d3 1 3' # noqa: E501
130 | PETROVS_DEFENCE__THREE_KNIGHTS_GAME = 'rnbqkb1r/pppp1ppp/5n2/4p3/4P3/2N2N2/PPPP1PPP/R1BQKB1R b KQkq - 4 3' # noqa: E501
131 | PHILIDOR_DEFENCE = 'rnbqkbnr/ppp2ppp/3p4/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 1 3' # noqa: E501
132 | PIRC_DEFENCE = 'rnbqkb1r/ppp1pppp/3p1n2/8/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 2 3' # noqa: E501
133 | PIRC_DEFENCE__AUSTRIAN_ATTACK = 'rnbqkb1r/ppp1pp1p/3p1np1/8/3PPP2/2N5/PPP3PP/R1BQKBNR b KQkq f3 1 4' # noqa: E501
134 | PIRC_DEFENCE__CLASSICAL_VARIATION = 'rnbqkb1r/ppp1pp1p/3p1np1/8/3PP3/2N2N2/PPP2PPP/R1BQKB1R b KQkq - 2 4' # noqa: E501
135 | QUEENS_GAMBIT = 'rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 1 2' # noqa: E501
136 | QUEENS_GAMBIT_ACCEPTED = 'rnbqkbnr/ppp1pppp/8/8/2pP4/8/PP2PPPP/RNBQKBNR w KQkq - 1 3' # noqa: E501
137 | QUEENS_GAMBIT_DECLINED__ALBIN_COUNTERGAMBIT = 'rnbqkbnr/ppp2ppp/8/3pp3/2PP4/8/PP2PPPP/RNBQKBNR w KQkq e6 1 3' # noqa: E501
138 | QUEENS_GAMBIT_DECLINED__CHIGORIN_DEFENCE = 'r1bqkbnr/ppp1pppp/2n5/3p4/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 2 3' # noqa: E501
139 | QUEENS_GAMBIT_DECLINED__SEMI_SLAV_DEFENCE = 'rnbqkb1r/pp3ppp/2p1pn2/3p4/2PP4/2N2N2/PP2PPPP/R1BQKB1R w KQkq - 1 5' # noqa: E501
140 | QUEENS_GAMBIT_DECLINED__SEMI_TARRASCH_DEFENCE = 'rnbqkb1r/pp3ppp/4pn2/2pp4/2PP4/2N2N2/PP2PPPP/R1BQKB1R w KQkq c6 1 5' # noqa: E501
141 | QUEENS_GAMBIT_DECLINED__SLAV_DEFENCE = 'rnbqkbnr/pp2pppp/2p5/3p4/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 3' # noqa: E501
142 | QUEENS_GAMBIT_DECLINED__TARRASCH_DEFENCE = 'rnbqkbnr/pp3ppp/4p3/2pp4/2PP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 0 4' # noqa: E501
143 | QUEENS_INDIAN_DEFENCE = 'rnbqkb1r/p1pp1ppp/1p2pn2/8/2PP4/5N2/PP2PPPP/RNBQKB1R w KQkq - 1 4' # noqa: E501
144 | QUEENS_PAWN = 'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq d3 1 1' # noqa: E501
145 | QUEENSS_PAWN_GAME__MODERN_DEFENCE = 'rnbqk1nr/ppp1ppbp/3p2p1/8/2PP4/2N5/PP2PPPP/R1BQKBNR w KQkq - 1 4' # noqa: E501
146 | RICHTER_VERESOV_ATTACK = 'rnbqkb1r/ppp1pppp/5n2/3p2B1/3P4/2N5/PPP1PPPP/R2QKBNR b KQkq - 4 3' # noqa: E501
147 | RUY_LOPEZ = 'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 4 3' # noqa: E501
148 | RUY_LOPEZ__BERLIN_DEFENCE = 'r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 5 4' # noqa: E501
149 | RUY_LOPEZ__CLASSICAL_VARIATION = 'r1bqk1nr/pppp1ppp/2n5/1Bb1p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 5 4' # noqa: E501
150 | RUY_LOPEZ__CLOSED_VARIATION = 'r1bqk2r/2ppbppp/p1n2n2/1p2p3/4P3/1B3N2/PPPP1PPP/RNBQR1K1 b kq - 0 7' # noqa: E501
151 | RUY_LOPEZ__EXCHANGE_VARIATION = 'r1bqkbnr/1ppp1ppp/p1B5/4p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 1 4' # noqa: E501
152 | RUY_LOPEZ__MARSHALL_ATTACK = 'r1bq1rk1/2p1bppp/p1n2n2/1p1pp3/4P3/1BP2N2/PP1P1PPP/RNBQR1K1 w - - 0 9' # noqa: E501
153 | RUY_LOPEZ__SCHLIEMANN_DEFENCE = 'r1bqkbnr/pppp2pp/2n5/1B2pp2/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq f6 1 4' # noqa: E501
154 | RETI_OPENING = 'rnbqkbnr/ppp1pppp/8/3p4/2P5/5N2/PP1PPPPP/RNBQKB1R b KQkq c3 1 2' # noqa: E501
155 | SCANDINAVIAN_DEFENCE = 'rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq d6 1 2' # noqa: E501
156 | SCANDINAVIAN_DEFENCE__MODERN_VARIATION = 'rnbqkb1r/ppp1pppp/5n2/3P4/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 0 3' # noqa: E501
157 | SCOTCH_GAME = 'r1bqkbnr/pppp1ppp/2n5/4p3/3PP3/5N2/PPP2PPP/RNBQKB1R b KQkq d3 1 3' # noqa: E501
158 | SCOTCH_GAME__CLASSICAL_VARIATION = 'r1bqk1nr/pppp1ppp/2n5/2b5/3NP3/8/PPP2PPP/RNBQKB1R w KQkq - 2 5' # noqa: E501
159 | SCOTCH_GAME__MIESES_VARIATION = 'r1bqkb1r/p1pp1ppp/2p2n2/4P3/8/8/PPP2PPP/RNBQKB1R b KQkq - 1 6' # noqa: E501
160 | SCOTCH_GAME__STEINITZ_VARIATION = 'r1b1kbnr/pppp1ppp/2n5/8/3NP2q/8/PPP2PPP/RNBQKB1R w KQkq - 2 5' # noqa: E501
161 | SICILIAN_DEFENCE = 'rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 1 2' # noqa: E501
162 | SICILIAN_DEFENCE__ACCELERATED_DRAGON = 'r1bqkbnr/pp1ppp1p/2n3p1/8/3NP3/8/PPP2PPP/RNBQKB1R w KQkq - 1 5' # noqa: E501
163 | SICILIAN_DEFENCE__ALAPIN_VARIATION = 'rnbqkbnr/pp1ppppp/8/2p5/4P3/2P5/PP1P1PPP/RNBQKBNR b KQkq - 1 2' # noqa: E501
164 | SICILIAN_DEFENCE__CLOSED_VARIATION = 'rnbqkbnr/pp1ppppp/8/2p5/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 2 2' # noqa: E501
165 | SICILIAN_DEFENCE__DRAGON_VARIATION = 'rnbqkb1r/pp2pp1p/3p1np1/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 1 6' # noqa: E501
166 | SICILIAN_DEFENCE__GRAND_PRIX_ATTACK = 'r1bqkbnr/pp1ppppp/2n5/2p5/4PP2/2N5/PPPP2PP/R1BQKBNR b KQkq f3 1 3' # noqa: E501
167 | SICILIAN_DEFENCE__HYPER_ACCELERATED_DRAGON = 'rnbqkbnr/pp1ppp1p/6p1/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 1 2' # noqa: E501
168 | SICILIAN_DEFENCE__KAN_VARIATION = 'rnbqkbnr/1p1p1ppp/p3p3/8/3NP3/8/PPP2PPP/RNBQKB1R w KQkq - 1 5' # noqa: E501
169 | SICILIAN_DEFENCE__NAJDORF_VARIATION = 'rnbqkb1r/1p2pppp/p2p1n2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 1 6' # noqa: E501
170 | SICILIAN_DEFENCE__RICHTER_RAUZER_VARIATION = 'r1bqkb1r/pp2pppp/2np1n2/6B1/3NP3/2N5/PPP2PPP/R2QKB1R b KQkq - 5 6' # noqa: E501
171 | SICILIAN_DEFENCE__SCHEVENINGEN_VARIATION = 'rnbqkb1r/pp3ppp/3ppn2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 1 6' # noqa: E501
172 | SICILIAN_DEFENCE__SMITH_MORRA_GAMBIT = 'rnbqkbnr/pp1ppppp/8/8/3pP3/2P5/PP3PPP/RNBQKBNR b KQkq - 1 3' # noqa: E501
173 | SOKOLSKY_OPENING = 'rnbqkbnr/pppppppp/8/8/1P6/8/P1PPPPPP/RNBQKBNR b KQkq - 1 1' # noqa: E501
174 | TORRE_ATTACK = 'rnbqkb1r/ppp1pppp/5n2/3p2B1/3P4/5N2/PPP1PPPP/RN1QKB1R b KQkq - 4 3' # noqa: E501
175 | TROMPOWSKY_ATTACK = 'rnbqkb1r/pppppppp/5n2/6B1/3P4/8/PPP1PPPP/RN1QKBNR b KQkq - 3 2' # noqa: E501
176 | VIENNA_GAME = 'rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 2 2' # noqa: E501
177 | ZUKERTORT_OPENING = 'rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1' # noqa: E501
178 |
--------------------------------------------------------------------------------
/berserk/exceptions.py:
--------------------------------------------------------------------------------
1 | def get_message(e):
2 | return e.args[0] if e.args else ''
3 |
4 |
5 | def set_message(e, value):
6 | args = list(e.args)
7 | if args:
8 | args[0] = value
9 | else:
10 | args.append(value)
11 | e.args = args
12 |
13 |
14 | class BerserkError(Exception):
15 | message = property(get_message, set_message)
16 |
17 |
18 | class ApiError(BerserkError):
19 | def __init__(self, error):
20 | super().__init__(get_message(error))
21 | self.__cause__ = self.error = error
22 |
23 |
24 | class ResponseError(ApiError):
25 | """Response that indicates an error."""
26 |
27 | # sentinal object for when None is a valid result
28 | __UNDEFINED = object()
29 |
30 | def __init__(self, response):
31 | error = ResponseError._catch_exception(response)
32 | super().__init__(error)
33 | self._cause = ResponseError.__UNDEFINED
34 | self.response = response
35 | base_message = f'HTTP {self.status_code}: {self.reason}'
36 | if self.cause:
37 | self.message = f'{base_message}: {self.cause}'
38 |
39 | @property
40 | def status_code(self):
41 | """HTTP status code of the response."""
42 | return self.response.status_code
43 |
44 | @property
45 | def reason(self):
46 | """HTTP status text of the response."""
47 | return self.response.reason
48 |
49 | @property
50 | def cause(self):
51 | if self._cause is ResponseError.__UNDEFINED:
52 | try:
53 | self._cause = self.response.json()
54 | except Exception:
55 | self._cause = None
56 | return self._cause
57 |
58 | @staticmethod
59 | def _catch_exception(response):
60 | try:
61 | response.raise_for_status()
62 | except Exception as e:
63 | return e
64 |
--------------------------------------------------------------------------------
/berserk/formats.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import json
3 |
4 | import ndjson
5 |
6 | from . import utils
7 |
8 |
9 | class FormatHandler:
10 | """Provide request headers and parse responses for a particular format.
11 |
12 | Instances of this class should override the :meth:`parse_stream` and
13 | :meth:`parse` methods to support handling both streaming and non-streaming
14 | responses.
15 |
16 | :param str mime_type: the MIME type for the format
17 | """
18 |
19 | def __init__(self, mime_type):
20 | self.mime_type = mime_type
21 | self.headers = {'Accept': mime_type}
22 |
23 | def handle(self, response, is_stream, converter=utils.noop):
24 | """Handle the response by returning the data.
25 |
26 | :param response: raw response
27 | :type response: :class:`requests.Response`
28 | :param bool is_stream: ``True`` if the response is a stream
29 | :param func converter: function to handle field conversions
30 | :return: either all response data or an iterator of response data
31 | """
32 | if is_stream:
33 | return map(converter, iter(self.parse_stream(response)))
34 | else:
35 | return converter(self.parse(response))
36 |
37 | def parse(self, response):
38 | """Parse all data from a response.
39 |
40 | :param response: raw response
41 | :type response: :class:`requests.Response`
42 | :return: response data
43 | """
44 | return response
45 |
46 | def parse_stream(self, response):
47 | """Yield the parsed data from a stream response.
48 |
49 | :param response: raw response
50 | :type response: :class:`requests.Response`
51 | :return: iterator over the response data
52 | """
53 | yield response
54 |
55 |
56 | class JsonHandler(FormatHandler):
57 | """Handle JSON data.
58 |
59 | :param str mime_type: the MIME type for the format
60 | :param decoder: the decoder to use for the JSON format
61 | :type decoder: :class:`json.JSONDecoder`
62 | """
63 |
64 | def __init__(self, mime_type, decoder=json.JSONDecoder):
65 | super().__init__(mime_type=mime_type)
66 | self.decoder = decoder
67 |
68 | def parse(self, response):
69 | """Parse all JSON data from a response.
70 |
71 | :param response: raw response
72 | :type response: :class:`requests.Response`
73 | :return: response data
74 | :rtype: JSON
75 | """
76 | return response.json(cls=self.decoder)
77 |
78 | def parse_stream(self, response):
79 | """Yield the parsed data from a stream response.
80 |
81 | :param response: raw response
82 | :type response: :class:`requests.Response`
83 | :return: iterator over multiple JSON objects
84 | """
85 | for line in response.iter_lines():
86 | if line:
87 | decoded_line = line.decode('utf-8')
88 | yield json.loads(decoded_line)
89 |
90 |
91 | class PgnHandler(FormatHandler):
92 | """Handle PGN data."""
93 |
94 | def __init__(self):
95 | super().__init__(mime_type='application/x-chess-pgn')
96 |
97 | def handle(self, *args, **kwargs):
98 | kwargs['converter'] = utils.noop # disable conversions
99 | return super().handle(*args, **kwargs)
100 |
101 | def parse(self, response):
102 | """Parse all text data from a response.
103 |
104 | :param response: raw response
105 | :type response: :class:`requests.Response`
106 | :return: response text
107 | :rtype: str
108 | """
109 | return response.text
110 |
111 | def parse_stream(self, response):
112 | """Yield the parsed PGN games from a stream response.
113 |
114 | :param response: raw response
115 | :type response: :class:`requests.Response`
116 | :return: iterator over multiple PGN texts
117 | """
118 | lines = []
119 | last_line = True
120 | for line in response.iter_lines():
121 | decoded_line = line.decode('utf-8')
122 | if last_line or decoded_line:
123 | lines.append(decoded_line)
124 | else:
125 | yield '\n'.join(lines).strip()
126 | lines = []
127 | last_line = decoded_line
128 |
129 | if lines:
130 | yield '\n'.join(lines).strip()
131 |
132 |
133 | class TextHandler(FormatHandler):
134 | def __init__(self):
135 | super().__init__(mime_type='text/plain')
136 |
137 | def parse(self, response):
138 | return response.text
139 |
140 | def parse_stream(self, response):
141 | yield from response.iter_lines()
142 |
143 |
144 | #: Basic text
145 | TEXT = TextHandler()
146 |
147 | #: Handles vanilla JSON
148 | JSON = JsonHandler(mime_type='application/json')
149 |
150 | #: Handles oddball LiChess JSON (normal JSON, crazy MIME type)
151 | LIJSON = JsonHandler(mime_type='application/vnd.lichess.v3+json')
152 |
153 | #: Handles newline-delimited JSON
154 | NDJSON = JsonHandler(mime_type='application/x-ndjson', decoder=ndjson.Decoder)
155 |
156 | #: Handles PGN
157 | PGN = PgnHandler()
158 |
--------------------------------------------------------------------------------
/berserk/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from . import utils
3 |
4 |
5 | class model(type):
6 | @property
7 | def conversions(cls):
8 | return {k: v for k, v in vars(cls).items() if not k.startswith('_')}
9 |
10 |
11 | class Model(metaclass=model):
12 | @classmethod
13 | def convert(cls, data):
14 | if isinstance(data, (list, tuple)):
15 | return [cls.convert_one(v) for v in data]
16 | return cls.convert_one(data)
17 |
18 | @classmethod
19 | def convert_one(cls, data):
20 | for k in set(data) & set(cls.conversions):
21 | data[k] = cls.conversions[k](data[k])
22 | return data
23 |
24 | @classmethod
25 | def convert_values(cls, data):
26 | for k in data:
27 | data[k] = cls.convert(data[k])
28 | return data
29 |
30 |
31 | class Account(Model):
32 | createdAt = utils.datetime_from_millis
33 | seenAt = utils.datetime_from_millis
34 |
35 |
36 | class User(Model):
37 | createdAt = utils.datetime_from_millis
38 | seenAt = utils.datetime_from_millis
39 |
40 |
41 | class Activity(Model):
42 | interval = utils.inner(utils.datetime_from_millis, 'start', 'end')
43 |
44 |
45 | class Game(Model):
46 | createdAt = utils.datetime_from_millis
47 | lastMoveAt = utils.datetime_from_millis
48 |
49 |
50 | class GameState(Model):
51 | createdAt = utils.datetime_from_millis
52 | wtime = utils.datetime_from_millis
53 | btime = utils.datetime_from_millis
54 | winc = utils.datetime_from_millis
55 | binc = utils.datetime_from_millis
56 |
57 |
58 | class Tournament(Model):
59 | startsAt = utils.datetime_from_str
60 |
61 |
62 | class Tournaments(Model):
63 | startsAt = utils.datetime_from_millis
64 | finishesAt = utils.datetime_from_millis
65 |
66 |
67 | class Broadcast(Model):
68 | broadcast = utils.inner(
69 | utils.datetime_from_millis, 'startedAt', 'startsAt'
70 | )
71 |
72 |
73 | class RatingHistory(Model):
74 | points = utils.listing(utils.rating_history)
75 |
76 |
77 | class PuzzleActivity(Model):
78 | date = utils.datetime_from_millis
79 |
--------------------------------------------------------------------------------
/berserk/session.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import urllib
4 |
5 | import requests
6 |
7 | from . import (
8 | exceptions,
9 | utils,
10 | )
11 |
12 | LOG = logging.getLogger(__name__)
13 |
14 |
15 | class Requestor:
16 | """Encapsulates the logic for making a request.
17 |
18 | :param session: the authenticated session object
19 | :type session: :class:`requests.Session`
20 | :param str base_url: the base URL for requests
21 | :param fmt: default format handler to use
22 | :type fmt: :class:`~berserk.formats.FormatHandler`
23 | """
24 |
25 | def __init__(self, session, base_url, default_fmt):
26 | self.session = session
27 | self.base_url = base_url
28 | self.default_fmt = default_fmt
29 |
30 | def request(
31 | self, method, path, *args, fmt=None, converter=utils.noop, **kwargs
32 | ):
33 | """Make a request for a resource in a paticular format.
34 |
35 | :param str method: HTTP verb
36 | :param str path: the URL suffix
37 | :param fmt: the format handler
38 | :type fmt: :class:`~berserk.formats.FormatHandler`
39 | :param func converter: function to handle field conversions
40 | :return: response
41 | :raises berserk.exceptions.ResponseError: if the status is >=400
42 | """
43 | fmt = fmt or self.default_fmt
44 | kwargs['headers'] = fmt.headers
45 | url = urllib.parse.urljoin(self.base_url, path)
46 |
47 | is_stream = kwargs.get('stream')
48 | LOG.debug(
49 | '%s %s %s params=%s data=%s json=%s',
50 | 'stream' if is_stream else 'request',
51 | method,
52 | url,
53 | kwargs.get('params'),
54 | kwargs.get('data'),
55 | kwargs.get('json'),
56 | )
57 | try:
58 | response = self.session.request(method, url, *args, **kwargs)
59 | except requests.RequestException as e:
60 | raise exceptions.ApiError(e)
61 | if not response.ok:
62 | raise exceptions.ResponseError(response)
63 |
64 | return fmt.handle(response, is_stream=is_stream, converter=converter)
65 |
66 | def get(self, *args, **kwargs):
67 | """Convenience method to make a GET request."""
68 | return self.request('GET', *args, **kwargs)
69 |
70 | def post(self, *args, **kwargs):
71 | """Convenience method to make a POST request."""
72 | return self.request('POST', *args, **kwargs)
73 |
74 |
75 | class TokenSession(requests.Session):
76 | """Session capable of personal API token authentication.
77 |
78 | :param str token: personal API token
79 | """
80 |
81 | def __init__(self, token):
82 | super().__init__()
83 | self.token = token
84 | self.headers = {'Authorization': f'Bearer {token}'}
85 |
--------------------------------------------------------------------------------
/berserk/todo.md:
--------------------------------------------------------------------------------
1 | # berserk
2 | ## TO-DO
3 | ### Account
4 | - [x] Get my profile
5 | - [x] Get my email adress
6 | - [x] Get my preferences
7 | - [x] Get my kid mode status
8 | - [x] Set my kid mode status
9 |
10 | _:warning: Found `upgrade_to_bot` in **Account** instead of **Bot**_
11 |
12 | ### Users
13 | - [x] Get real-time users status
14 | - [x] Get all top 10
15 | - [x] Get one leaderboard
16 | - [x] Get user public data
17 | - [x] Get rating history of a user
18 | - [x] Get performance statistics of a user
19 | - [x] Get user activity
20 | - [x] Get users by ID
21 | - [x] Get members of a team *:warning: Depreated*
22 | - [x] Get live streamers
23 | - [x] Get crosstable
24 |
25 | _:warning: Found `get_users_following` and `get_users_followed` in **Users** instead of **Relations** :information_source: `deprecated`_
26 |
27 | ### Relations
28 | - [x] Get users followed by a user _:warning: Found in **Users** :information_source: `deprecated`_
29 | - [x] Get users who follow a user _:warning: Found in **Users** :information_source: `deprecated`_
30 | - [x] Follow a player
31 | - [x] Unfollow a player
32 |
33 | ### Games
34 | - [x] Export one game
35 | - [x] Export ongoing game of a user
36 | - [x] Export games of a user
37 | - [x] Export games by IDs
38 | - [x] Stream current games
39 | - [x] Get my ongoing games
40 | - [x] Stream moves of a game
41 | - [x] Import one game
42 |
43 | _:warning: Found `get_tv_channels` in **Games** instead of **TV** :information_source: `deprecated`__
44 |
45 | ### TV
46 | - [x] Get current TV games _:warning: Found in **Games** :information_source: `deprecated`_
47 | - [x] Stream current TV game
48 | - [x] Get best ongoing games of a TV channel
49 |
50 | ### Puzzles
51 | - [x] Get the daily puzzle
52 | - [x] Get your puzzle activity
53 | - [x] Get your puzzle dashboard
54 | - [x] Get the storm dashboard of a player
55 |
56 | ### :heavy_check_mark: Teams
57 | - [x] Get team swiss tournaments
58 | - [x] Get a single team
59 | - [x] Get popular teams
60 | - [x] Teams of a player
61 | - [x] Search teams
62 | - [x] Get members of a team
63 | - [x] Get team Arena tournaments
64 | - [x] Join a team
65 | - [x] Leave a team
66 | - [x] Kick a user from your team
67 | - [ ] Message all members
68 |
69 | ### Board
70 | - [x] Stream incoming events
71 | - [x] Create a seek
72 | - [x] Stream Board game state
73 | - [x] Make a Board move
74 | - [x] Write in the chat
75 | - [ ] Fetch the game chat
76 | - [x] Abort a game
77 | - [x] Resign a game
78 | - [x] Handle draw offers
79 | - [ ] Handle takeback offers
80 | - [ ] Claim victory of a game
81 |
82 | ### :heavy_check_mark: Bots
83 | - [x] Stream incoming events
84 | - [x] Get online bots
85 | - [x] Upgrade to Bot account
86 | - [x] Stream Bot game state
87 | - [x] Make a Bot move
88 | - [x] Write in the chat
89 | - [x] Abort a game
90 | - [x] Resign a game
91 |
92 | _:warning: Found `accept_challenge` and `decline_challenge` in **Bots** instead of **Challenges**_
93 |
94 | ### Challenges
95 | - [ ] List your challenges
96 | - [x] Create a challenge
97 | - [x] Accept a challenge
98 | - [x] Decline a challenge
99 | - [ ] Cancel a challenge
100 | - [x] Challenge the AI
101 | - [x] Open-ended challenge
102 | - [ ] Start clocks of a game
103 | - [ ] Add time to the opponent clock
104 | - [ ] Admin challenge tokens
105 |
106 | ### Bulk pairings
107 | - [ ] View upcoming bulk pairings
108 | - [ ] Create a bulk pairing
109 | - [ ] Manually start clocks
110 | - [ ] Cancel a bulk pairing
111 |
112 | ### Arena tournaments
113 | - [x] Get current tournaments
114 | - [x] Create a new Arena tournament
115 | - [ ] Get info about an Arena tournament
116 | - [ ] Update an Arena tournament
117 | - [ ] Join an Arena tournament
118 | - [ ] Terminate an Arena tournament
119 | - [ ] Update a team battle
120 | - [x] Export games of an Arena tournament
121 | - [x] Get results of an Arena tournament
122 | - [ ] Get team standing of a team battle
123 | - [x] Get tournaments created by a user
124 | - [ ] Get team Arena tournaments
125 |
126 | ### Swiss
127 |
128 | ### Opening Explorer
129 | - [x] Masters database
130 | - [x] Lichess games
131 | - [x] Player games
132 | - [ ] OTB master game
--------------------------------------------------------------------------------
/berserk/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import collections
3 | from datetime import (
4 | datetime,
5 | timezone,
6 | )
7 |
8 |
9 | def to_millis(dt):
10 | """Return the milliseconds between the given datetime and the epoch.
11 |
12 | :param datetime dt: a datetime
13 | :return: milliseconds since the epoch
14 | :rtype: int
15 | """
16 | return dt.timestamp() * 1000
17 |
18 |
19 | def datetime_from_seconds(ts):
20 | """Return the datetime for the given seconds since the epoch.
21 |
22 | UTC is assumed. The returned datetime is timezone aware.
23 |
24 | :return: timezone aware datetime
25 | :rtype: :class:`datetime`
26 | """
27 | return datetime.fromtimestamp(ts, timezone.utc)
28 |
29 |
30 | def datetime_from_millis(millis):
31 | """Return the datetime for the given millis since the epoch.
32 |
33 | UTC is assumed. The returned datetime is timezone aware.
34 |
35 | :return: timezone aware datetime
36 | :rtype: :class:`datetime`
37 | """
38 | return datetime_from_seconds(millis / 1000)
39 |
40 |
41 | def datetime_from_str(dt_str):
42 | """Convert the time in a string to a datetime.
43 |
44 | UTC is assumed. The returned datetime is timezone aware. The format
45 | must match ``%Y-%m-%dT%H:%M:%S.%fZ``.
46 |
47 | :return: timezone aware datetime
48 | :rtype: :class:`datetime`
49 | """
50 | dt = datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S.%fZ')
51 | return dt.replace(tzinfo=timezone.utc)
52 |
53 |
54 | _RatingHistoryEntry = collections.namedtuple('Entry', 'year month day rating')
55 |
56 |
57 | def rating_history(data):
58 | return _RatingHistoryEntry(*data)
59 |
60 |
61 | def inner(func, *keys):
62 | def convert(data):
63 | for k in keys:
64 | try:
65 | data[k] = func(data[k])
66 | except KeyError:
67 | pass # normal for keys to not be present sometimes
68 | return data
69 |
70 | return convert
71 |
72 |
73 | def listing(func):
74 | def convert(items):
75 | result = []
76 | for item in items:
77 | result.append(func(item))
78 | return result
79 |
80 | return convert
81 |
82 |
83 | def noop(arg):
84 | return arg
85 |
86 |
87 | def build_adapter(mapper, sep='.'):
88 | """Build a data adapter.
89 |
90 | Uses a map to pull values from an object and assign them to keys.
91 | For example:
92 |
93 | .. code-block:: python
94 |
95 | >>> mapping = {
96 | ... 'broadcast_id': 'broadcast.id',
97 | ... 'slug': 'broadcast.slug',
98 | ... 'name': 'broadcast.name',
99 | ... 'description': 'broadcast.description',
100 | ... 'syncUrl': 'broadcast.sync.url',
101 | ... }
102 |
103 | >>> cast = {'broadcast': {'id': 'WxOb8OUT',
104 | ... 'slug': 'test-tourney',
105 | ... 'name': 'Test Tourney',
106 | ... 'description': 'Just a test',
107 | ... 'ownerId': 'rhgrant10',
108 | ... 'sync': {'ongoing': False, 'log': [], 'url': None}},
109 | ... 'url': 'https://lichess.org/broadcast/test-tourney/WxOb8OUT'}
110 |
111 | >>> adapt = build_adapter(mapping)
112 | >>> adapt(cast)
113 | {'broadcast_id': 'WxOb8OUT',
114 | 'slug': 'test-tourney',
115 | 'name': 'Test Tourney',
116 | 'description': 'Just a test',
117 | 'syncUrl': None}
118 |
119 | :param dict mapper: map of keys to their location in an object
120 | :param str sep: nested key delimiter
121 | :return: adapted data
122 | :rtype: dict
123 | """
124 |
125 | def get(data, location):
126 | for key in location.split(sep):
127 | data = data[key]
128 | return data
129 |
130 | def adapter(data, default=None, fill=False):
131 | result = {}
132 | for key, loc in mapper.items():
133 | try:
134 | result[key] = get(data, loc)
135 | except KeyError:
136 | if fill:
137 | result[key] = default
138 | return result
139 |
140 | return adapter
141 |
142 |
143 | def page(
144 | get_page,
145 | args=None,
146 | kwargs=None,
147 | results_key='currentPageResults',
148 | page_param='page',
149 | first_page=1,
150 | page_increment=1,
151 | last_page=None,
152 | ):
153 | results = True
154 | kwargs = kwargs or {}
155 | kwargs.setdefault(page_param, first_page)
156 | while results and not last_page or kwargs[page_param] <= last_page:
157 | page = get_page(*args, **kwargs)
158 | results = page[results_key]
159 | yield from results
160 | kwargs[page_param] += page_increment
161 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | patch:
4 | default: false
5 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = berserk
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/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | .highlight .go {
2 | color: hsla(0, 0%, 50%, .8);
3 | }
--------------------------------------------------------------------------------
/docs/announcing.rst:
--------------------------------------------------------------------------------
1 | Hi everyone,
2 |
3 | I'm pleased to announce the release of berserk v0.7.0!
4 |
5 | What's New?
6 | -----------
7 |
8 | It's been a while since the last slew of commits and lots has happened since v0.3.2:
9 |
10 | **Features**
11 |
12 | * Add ``ApiError`` for all other request errors
13 | * Add ``ResponseError`` for 4xx and 5xx responses with status code, reason, and cause
14 | * Add a utility for easily converting API objects into update params
15 | * Add logging to the ``berserk.session`` module
16 | * Add new ``Teams`` client: join, get members, kick member, and leave
17 | * Add simuls
18 | * Add studies export and export chapter
19 | * Add support for the broadcast endpoints
20 | * Add tests for all utils
21 | * Add tournament results, games export, and list by creator
22 | * Add user followers, users following, rating history, and puzzle activity
23 |
24 | **Deprecations**
25 |
26 | * Deprecated ``Users.get_by_team`` - use ``Teams.get_members`` instead
27 |
28 | **Bugfixes**
29 |
30 | * Fix bug in ``Broadcasts.push_pgn_update``
31 | * Fix exception message when no cause
32 | * Fix multiple bugs with the tournament create endpoint
33 | * Fix py36 issue preventing successful build
34 | * Fix test case broken by 0.4.0 release
35 | * Fix multiple bugs in ``Tournaments.export_games``
36 |
37 | **Misc**
38 |
39 | * Update development status classifier to 4 - Beta
40 | * Update documentation and tweak the theme
41 | * Update the travis build to include py37
42 | * Update the Makefile
43 |
44 |
45 | What is berserk?
46 | ----------------
47 |
48 | berserk is the Python client for the Lichess API. It supports JSON and PGN,
49 | provides pluggable session auth, and implements most if not all of the API.
50 |
51 | License: GNU General Public License v3
52 |
53 | * Read the **docs**: https://berserk.readthedocs.io/
54 | * Install from **PyPI**: https://pypi.org/project/berserk/
55 | * Contribute **source**: https://github.com/rhgrant10/berserk
56 |
57 |
58 | Example
59 | -------
60 |
61 | .. code-block:: python
62 |
63 | >>> import berserk
64 |
65 | >>> session = berserk.TokenSession('my-api-token')
66 | >>> client = berserk.Client(session)
67 |
68 | >>> my = client.account.get()
69 | >>> games = list(client.games.export_by_player(my['username'], as_pgn=True))
70 | >>> len(games)
71 | 18
72 |
73 |
74 | Enjoy!
75 |
76 | -- Rob
77 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | Developer Interface
2 | ===================
3 |
4 | Clients
5 | -------
6 |
7 | .. automodule:: berserk.clients
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | Session
13 | -------
14 |
15 | .. automodule:: berserk.session
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
20 | Enums
21 | -----
22 |
23 | .. automodule:: berserk.enums
24 | :members:
25 | :undoc-members:
26 | :inherited-members:
27 | :show-inheritance:
28 |
29 | Formats
30 | -------
31 |
32 | .. automodule:: berserk.formats
33 | :members:
34 | :undoc-members:
35 | :show-inheritance:
36 |
37 |
38 | Exceptions
39 | ----------
40 |
41 | .. automodule:: berserk.exceptions
42 | :members:
43 | :undoc-members:
44 | :show-inheritance:
45 |
46 | Utils
47 | -----
48 |
49 | .. automodule:: berserk.utils
50 | :members:
51 |
--------------------------------------------------------------------------------
/docs/authors.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../AUTHORS.rst
2 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # berserk documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another
17 | # directory, add these directories to sys.path here. If the directory is
18 | # relative to the documentation root, use os.path.abspath to make it
19 | # absolute, like shown here.
20 | #
21 | import os
22 | import sys
23 |
24 | sys.path.insert(0, os.path.abspath('..'))
25 |
26 | import berserk # noqa
27 |
28 | # -- General configuration ---------------------------------------------
29 |
30 | # If your documentation needs a minimal Sphinx version, state it here.
31 | #
32 | # needs_sphinx = '1.0'
33 |
34 | # Add any Sphinx extension module names here, as strings. They can be
35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
36 | extensions = [
37 | 'sphinx.ext.autodoc',
38 | 'sphinx.ext.viewcode',
39 | 'sphinx.ext.intersphinx',
40 | ]
41 |
42 | # Add any paths that contain templates here, relative to this directory.
43 | templates_path = ['_templates']
44 |
45 | # The suffix(es) of source filenames.
46 | # You can specify multiple suffix as a list of string:
47 | #
48 | # source_suffix = ['.rst', '.md']
49 | source_suffix = '.rst'
50 |
51 | # The master toctree document.
52 | master_doc = 'index'
53 |
54 | # General information about the project.
55 | project = u'berserk'
56 | copyright = u"2018, Robert Grant"
57 | author = u"Robert Grant"
58 |
59 | # The version info for the project you're documenting, acts as replacement
60 | # for |version| and |release|, also used in various other places throughout
61 | # the built documents.
62 | #
63 | # The short X.Y version.
64 | version = berserk.__version__
65 | # The full version, including alpha/beta/rc tags.
66 | release = berserk.__version__
67 |
68 | # The language for content autogenerated by Sphinx. Refer to documentation
69 | # for a list of supported languages.
70 | #
71 | # This is also used if you do content translation via gettext catalogs.
72 | # Usually you set "language" from the command line for these cases.
73 | language = None
74 |
75 | # List of patterns, relative to source directory, that match files and
76 | # directories to ignore when looking for source files.
77 | # This patterns also effect to html_static_path and html_extra_path
78 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
79 |
80 | # The name of the Pygments (syntax highlighting) style to use.
81 | pygments_style = 'sphinx'
82 |
83 | # If true, `todo` and `todoList` produce output, else they produce nothing.
84 | todo_include_todos = False
85 |
86 |
87 | # -- Options for HTML output -------------------------------------------
88 |
89 | # The theme to use for HTML and HTML Help pages. See the documentation for
90 | # a list of builtin themes.
91 | #
92 | # html_theme = 'alabaster'
93 | html_theme = 'sphinx_rtd_theme'
94 |
95 | # Theme options are theme-specific and customize the look and feel of a
96 | # theme further. For a list of options available for each theme, see the
97 | # documentation.
98 | #
99 | html_theme_options = {
100 | # 'canonical_url': '',
101 | # 'analytics_id': 'UA-XXXXXXX-1', # Provided by Google in your dashboard
102 | # 'logo_only': False,
103 | 'display_version': True,
104 | # 'prev_next_buttons_location': 'bottom',
105 | # 'style_external_links': False,
106 | # 'vcs_pageview_mode': '',
107 | # 'style_nav_header_background': 'white',
108 | # # Toc options
109 | 'collapse_navigation': False,
110 | # 'sticky_navigation': True,
111 | # 'navigation_depth': 4,
112 | # 'includehidden': True,
113 | # 'titles_only': False
114 | }
115 |
116 | # Add any paths that contain custom static files (such as style sheets) here,
117 | # relative to this directory. They are copied after the builtin static files,
118 | # so a file named "default.css" will overwrite the builtin "default.css".
119 | html_static_path = ['_static']
120 | html_css_files = [
121 | 'css/custom.css',
122 | ]
123 |
124 | # -- Options for HTMLHelp output ---------------------------------------
125 |
126 | # Output file base name for HTML help builder.
127 | htmlhelp_basename = 'berserkdoc'
128 |
129 |
130 | # -- Options for LaTeX output ------------------------------------------
131 |
132 | latex_elements = {
133 | # The paper size ('letterpaper' or 'a4paper').
134 | #
135 | # 'papersize': 'letterpaper',
136 | # The font size ('10pt', '11pt' or '12pt').
137 | #
138 | # 'pointsize': '10pt',
139 | # Additional stuff for the LaTeX preamble.
140 | #
141 | # 'preamble': '',
142 | # Latex figure (float) alignment
143 | #
144 | # 'figure_align': 'htbp',
145 | }
146 |
147 | # Grouping the document tree into LaTeX files. List of tuples
148 | # (source start file, target name, title, author, documentclass
149 | # [howto, manual, or own class]).
150 | latex_documents = [
151 | (
152 | master_doc,
153 | 'berserk.tex',
154 | u'berserk Documentation',
155 | u'Robert Grant',
156 | 'manual',
157 | ),
158 | ]
159 |
160 |
161 | # -- Options for manual page output ------------------------------------
162 |
163 | # One entry per manual page. List of tuples
164 | # (source start file, name, description, authors, manual section).
165 | man_pages = [(master_doc, 'berserk', u'berserk Documentation', [author], 1)]
166 |
167 |
168 | # -- Options for Texinfo output ----------------------------------------
169 |
170 | # Grouping the document tree into Texinfo files. List of tuples
171 | # (source start file, target name, title, author,
172 | # dir menu entry, description, category)
173 | texinfo_documents = [
174 | (
175 | master_doc,
176 | 'berserk',
177 | u'berserk Documentation',
178 | author,
179 | 'berserk',
180 | 'One line description of project.',
181 | 'Miscellaneous',
182 | ),
183 | ]
184 |
185 |
186 | # Example configuration for intersphinx: refer to the Python standard library.
187 | intersphinx_mapping = {
188 | 'http://docs.python.org/3/': None,
189 | 'https://requests.readthedocs.io/en/master/': None,
190 | }
191 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/docs/history.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../HISTORY.rst
2 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to berserk's documentation!
2 | ======================================
3 |
4 | .. image:: https://img.shields.io/pypi/v/berserk.svg
5 | :target: https://pypi.python.org/pypi/berserk
6 | :alt: Available on PyPI
7 |
8 | .. image:: https://img.shields.io/travis/rhgrant10/berserk.svg
9 | :target: https://travis-ci.org/rhgrant10/berserk
10 | :alt: Continuous Integration
11 |
12 | .. image:: https://codecov.io/gh/rhgrant10/tsplib95/branch/master/graph/badge.svg
13 | :target: https://codecov.io/gh/rhgrant10/tsplib95
14 | :alt: Code Coverage
15 |
16 | .. image:: https://readthedocs.org/projects/berserk/badge/?version=latest
17 | :target: https://berserk.readthedocs.io/en/latest/?badge=latest
18 | :alt: Documentation Status
19 |
20 |
21 | Python client for the `Lichess API`_.
22 |
23 | .. _Lichess API: https://lichess.org/api
24 |
25 | ----
26 |
27 | .. toctree::
28 | :maxdepth: 4
29 |
30 | readme
31 | installation
32 | usage
33 | api
34 | contributing
35 | authors
36 | history
37 |
38 |
39 | Indices and tables
40 | ==================
41 | * :ref:`genindex`
42 | * :ref:`modindex`
43 | * :ref:`search`
44 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | Installation
4 | ============
5 |
6 |
7 | Stable release
8 | --------------
9 |
10 | To install berserk, run this command in your terminal:
11 |
12 | .. code-block:: console
13 |
14 | $ pip install berserk
15 |
16 | This is the preferred method to install berserk, as it will always install the most recent stable release.
17 |
18 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide
19 | you through the process.
20 |
21 | .. _pip: https://pip.pypa.io
22 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
23 |
24 |
25 | From sources
26 | ------------
27 |
28 | The sources for berserk can be downloaded from the `Github repo`_.
29 |
30 | You can either clone the public repository:
31 |
32 | .. code-block:: console
33 |
34 | $ git clone git://github.com/rhgrant10/berserk
35 |
36 | Or download the `tarball`_:
37 |
38 | .. code-block:: console
39 |
40 | $ curl -OL https://github.com/rhgrant10/berserk/tarball/master
41 |
42 | Once you have a copy of the source, you can install it with:
43 |
44 | .. code-block:: console
45 |
46 | $ python setup.py install
47 |
48 |
49 | .. _Github repo: https://github.com/rhgrant10/berserk
50 | .. _tarball: https://github.com/rhgrant10/berserk/tarball/master
51 |
--------------------------------------------------------------------------------
/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=python -msphinx
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=berserk
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/readme.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Usage
3 | =====
4 |
5 | Use ``berserk`` by creating an API client:
6 |
7 | .. code-block:: python
8 |
9 | >>> import berserk
10 | >>> client = berserk.Client()
11 |
12 | Authenticating
13 | ==============
14 |
15 | By default the client does not perform any authentication. However many of the
16 | endpoints are not open. To use a form of authentication, just pass the
17 | appropriate ``requests.Session``-like object:
18 |
19 | - using an API token: ``berserk.TokenSession``
20 | - using oauth: ``requests_oauthlib.Oauth2Session``
21 |
22 | .. note::
23 |
24 | Some endpoints require specific Oauth2 permissions.
25 |
26 | Using an API token
27 | ------------------
28 |
29 | If you have a personal API token, you can simply use the ``TokenSession``
30 | provided. For example, assuming you have written your token to
31 | ``'./lichess.token'``:
32 |
33 | .. code-block:: python
34 |
35 | >>> with open('./lichess.token') as f:
36 | ... token = f.read()
37 | ...
38 | >>> session = berserk.TokenSession(token)
39 | >>> client = berserk.Client(session)
40 |
41 | Using Oauth2
42 | ------------
43 |
44 | Some of the endpoints require OAuth2 authentication. Although outside the
45 | scope of this documentation, you can use ``requests_oauthlib.Oauth2Session``
46 | for this.
47 |
48 | .. code-block:: python
49 |
50 | >>> from requests_oauthlib import OAuth2Session
51 | >>> session = OAuth2Session(...)
52 | >>> client = berserk.Client(session)
53 |
54 |
55 | Accounts
56 | ========
57 |
58 | Information and Preferences
59 | ---------------------------
60 |
61 | .. code-block:: python
62 |
63 | >>> client.account.get()
64 | {'blocking': False,
65 | 'count': {...},
66 | 'createdAt': datetime.datetime(2018, 5, 16, 8, 9, 18, 187000),
67 | 'followable': True,
68 | 'following': False,
69 | 'followsYou': False,
70 | 'id': 'rhgrant10',
71 | 'nbFollowers': 1,
72 | 'nbFollowing': 1,
73 | 'online': True,
74 | 'perfs': {...},
75 | 'playTime': {...},
76 | 'seenAt': datetime.datetime(2018, 12, 9, 10, 28, 30, 221000),
77 | 'url': 'https://lichess.org/@/rhgrant10',
78 | 'username': 'rhgrant10'}
79 |
80 | >>> client.account.get_email()
81 | 'rhgrant10@gmail.com'
82 |
83 | >>> client.account.get_preferences()
84 | {'animation': 2,
85 | 'autoQueen': 1,
86 | ...
87 | 'transp': False,
88 | 'zen': 0}}
89 |
90 | Kid Mode
91 | --------
92 |
93 | Using Oauth2, you can set the kid mode.
94 |
95 | .. code-block:: python
96 |
97 | >>> client.account.set_kid_mode(True) # enable
98 | True
99 | >>> client.account.set_kid_mode(False) # disable
100 | True
101 |
102 | Note that the ``set_kid_mode`` method returns an indicator of success and *not*
103 | the current or previous status.
104 |
105 | .. code-block:: python
106 |
107 | >>> def show_kid_mode():
108 | ... is_enabled = client.account.get_kid_mode()
109 | ... print('enabled' if is_enabled else 'disabled')
110 | ...
111 | >>> show_kid_mode()
112 | disabled
113 |
114 | >>> # try to enable, but the request fails
115 | >>> client.account.set_kid_mode(True)
116 | False
117 | >>> show_kid_mode()
118 | disabled
119 |
120 | >>> # try again, this time it succeeds
121 | >>> client.account.set_kid_mode(True)
122 | True
123 | >>> show_kid_mode()
124 | enabled
125 |
126 | Bot Account Upgrade
127 | -------------------
128 |
129 | If this is a new account that has not yet played a game, and if you
130 | have the required OAuth2 permission, you can upgrade the account to a bot
131 | account:
132 |
133 | .. code-block:: python
134 |
135 | >>> client.account.upgrade_to_bot()
136 |
137 | Read more below about how to use bot functionality.
138 |
139 |
140 | Users and Teams
141 | ===============
142 |
143 | Realtime Statuses
144 | -----------------
145 |
146 | Get realtime information about one or more players:
147 |
148 | .. code-block:: python
149 |
150 | >>> players = ['Sasageyo', 'Voinikonis_Nikita', 'Zugzwangerz', 'DOES-NOT-EXIST']
151 | >>> client.users.get_realtime_statuses(players)
152 | [{'id': 'sasageyo',
153 | 'name': 'Sasageyo',
154 | 'title': 'IM',
155 | 'online': True,
156 | 'playing': True},
157 | {'id': 'voinikonis_nikita',
158 | 'name': 'Voinikonis_Nikita',
159 | 'title': 'FM',
160 | 'online': True,
161 | 'playing': True},
162 | {'id': 'zugzwangerz', 'name': 'Zugzwangerz'}]
163 |
164 | Top 10 Lists
165 | ------------
166 |
167 | .. code-block:: python
168 |
169 | >>> top10 = client.users.get_all_top_10()
170 | >>> list(top10)
171 | ['bullet',
172 | 'blitz',
173 | 'rapid',
174 | 'classical',
175 | 'ultraBullet',
176 | 'crazyhouse',
177 | 'chess960',
178 | 'kingOfTheHill',
179 | 'threeCheck',
180 | 'antichess',
181 | 'atomic',
182 | 'horde',
183 | 'racingKings']
184 | >>> top10['horde'][0]
185 | {'id': 'ingrid-vengeance',
186 | 'perfs': {'horde': {'progress': 22, 'rating': 2443}},
187 | 'username': 'Ingrid-Vengeance'}
188 |
189 | Leaderboards
190 | ------------
191 |
192 | .. code-block:: python
193 |
194 | >>> client.users.get_leaderboard('horde', count=11)[-1]
195 | {'id': 'philippesaner',
196 | 'perfs': {'horde': {'progress': 10, 'rating': 2230}},
197 | 'username': 'PhilippeSaner'}
198 |
199 | Public Data
200 | -----------
201 |
202 | .. code-block:: python
203 |
204 | >>> client.users.get_public_data('PhilippeSaner')
205 | {'completionRate': 87,
206 | 'count': {...},
207 | 'createdAt': datetime.datetime(2017, 1, 9, 16, 14, 31, 140000),
208 | 'id': 'philippesaner',
209 | 'nbFollowers': 40,
210 | 'nbFollowing': 13,
211 | 'online': False,
212 | 'perfs': {...},
213 | 'playTime': {'total': 1505020, 'tv': 1038007},
214 | 'profile': {'country': 'CA', 'location': 'Ottawa'},
215 | 'seenAt': datetime.datetime(2018, 12, 9, 10, 26, 28, 22000),
216 | 'url': 'https://lichess.org/@/PhilippeSaner',
217 | 'username': 'PhilippeSaner'}
218 |
219 | Activity Feeds
220 | --------------
221 |
222 | .. code-block:: python
223 |
224 | >>> feed = client.users.get_activity_feed('PhilippeSaner')
225 | >>> feed[0]
226 | {'games': {'horde': {'draw': 0,
227 | 'loss': 1,
228 | 'rp': {'after': 2230, 'before': 2198},
229 | 'win': 12}},
230 | 'interval': {'end': datetime.datetime(2018, 12, 9, 16, 0),
231 | 'start': datetime.datetime(2018, 12, 8, 16, 0)},
232 | 'tournaments': {'best': [{'nbGames': 1,
233 | 'rank': 6,
234 | 'rankPercent': 33,
235 | 'score': 2,
236 | 'tournament': {'id': '9zm2uIdP', 'name': 'Daily Horde Arena'}}],
237 | 'nb': 1}}
238 |
239 | Team Members
240 | ------------
241 |
242 | .. code-block:: python
243 |
244 | >>> client.users.get_by_team('coders')
245 |