diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0becf43..96aa9f7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,18 @@ Changelog ========= +Next release +------------ + +* Added ``client.bulk_pairings`` to create and manage bulk pairings + * ``client.bulk_pairings.get_upcoming`` to get upcoming bulk pairings you created + * ``client.bulk_pairings.create`` to create a bulk pairing + * ``client.bulk_pairings.start_clocks`` to start the clocks of a bulk pairing + * ``client.bulk_pairings.cancel`` to cancel a bulk pairing +* Added better return types for ``client.account.get`` and ``client.account.get_preferences`` + +Thanks to @Virinas-code for their contributions to this release. + v0.12.8 (2023-07-25) -------------------- diff --git a/README.rst b/README.rst index f0588c1..4ec3923 100644 --- a/README.rst +++ b/README.rst @@ -106,6 +106,11 @@ Most of the API is available: client.broadcasts.get_round_pgns client.broadcasts.get_pgns + client.bulk_pairings.get_upcoming + client.bulk_pairings.create + client.bulk_pairings.start_clocks + client.bulk_pairings.cancel + client.challenges.create client.challenges.create_ai client.challenges.create_open diff --git a/berserk/clients/__init__.py b/berserk/clients/__init__.py index 4ecb3b8..22df53b 100644 --- a/berserk/clients/__init__.py +++ b/berserk/clients/__init__.py @@ -21,6 +21,7 @@ from .tv import TV from .tablebase import Tablebase from .opening_explorer import OpeningExplorer +from .bulk_pairings import BulkPairings __all__ = [ "Client", @@ -41,6 +42,7 @@ "OAuth", "TV", "Tablebase", + "BulkPairings", ] @@ -64,6 +66,7 @@ class Client(BaseClient): - :class:`messaging ` - private message other players - :class:`tv ` - get information on tv channels and games - :class:`tablebase ` - lookup endgame tablebase + - :class:`bulk_pairings ` - manage bulk pairings :param session: request session, authenticated as needed :param base_url: base API URL to use (if other than the default) @@ -102,3 +105,4 @@ def __init__( self.tv = TV(session, base_url) self.tablebase = Tablebase(session, tablebase_url) self.opening_explorer = OpeningExplorer(session, explorer_url) + self.bulk_pairings = BulkPairings(session, base_url) diff --git a/berserk/clients/account.py b/berserk/clients/account.py index 789c9a4..434785b 100644 --- a/berserk/clients/account.py +++ b/berserk/clients/account.py @@ -1,21 +1,24 @@ from __future__ import annotations -from typing import Any, Dict +from typing import cast from .. import models +from ..types.account import AccountInformation, Preferences from .base import BaseClient class Account(BaseClient): """Client for account-related endpoints.""" - def get(self) -> Dict[str, Any]: + def get(self) -> AccountInformation: """Get your public information. :return: public information about the authenticated user """ path = "/api/account" - return self._r.get(path, converter=models.Account.convert) + return cast( + AccountInformation, self._r.get(path, converter=models.Account.convert) + ) def get_email(self) -> str: """Get your email address. @@ -25,13 +28,13 @@ def get_email(self) -> str: path = "/api/account/email" return self._r.get(path)["email"] - def get_preferences(self) -> Dict[str, Any]: + def get_preferences(self) -> Preferences: """Get your account preferences. :return: preferences of the authenticated user """ path = "/api/account/preferences" - return self._r.get(path)["prefs"] + return cast(Preferences, self._r.get(path)) def get_kid_mode(self) -> bool: """Get your kid mode status. @@ -41,7 +44,7 @@ def get_kid_mode(self) -> bool: path = "/api/account/kid" return self._r.get(path)["kid"] - def set_kid_mode(self, value: bool): + def set_kid_mode(self, value: bool) -> None: """Set your kid mode status. :param bool value: whether to enable or disable kid mode diff --git a/berserk/clients/bulk_pairings.py b/berserk/clients/bulk_pairings.py new file mode 100644 index 0000000..4d36d1a --- /dev/null +++ b/berserk/clients/bulk_pairings.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from typing import cast + +from ..enums import Variant +from ..formats import JSON, JSON_LIST +from ..types.bulk_pairings import BulkPairing +from .base import BaseClient + + +class BulkPairings(BaseClient): + """Client for bulk pairing related endpoints.""" + + def get_upcoming(self) -> list[BulkPairing]: + """Get a list of upcoming bulk pairings you created. + + Only bulk pairings that are scheduled in the future, or that have a clock start scheduled in the future, are listed. + + Bulk pairings are deleted from the server after the pairings are done and the clocks have started. + + :return: list of your upcoming bulk pairings. + """ + path = "/api/bulk-pairing" + return cast("list[BulkPairing]", self._r.get(path, fmt=JSON_LIST)) + + def create( + self, + token_pairings: list[tuple[str, str]], + clock_limit: int | None = None, + clock_increment: int | None = None, + days: int | None = None, + pair_at: int | None = None, + start_clocks_at: int | None = None, + rated: bool = False, + variant: Variant | None = None, + fen: str | None = None, + message: str | None = None, + rules: list[str] | None = None, + ) -> BulkPairing: + """Create a bulk pairing. + + :param players_tokens: players OAuth tokens + :param clock_limit: clock initial time + :param clock_increment: clock increment + :param days: days per turn (for correspondence) + :param pair_at: date at which the games will be created as a milliseconds unix timestamp. Up to 7 days in the future. Defaults to now. + :param start_clocks_at: date at which the clocks will be automatically started as a Unix timestamp in milliseconds. + Up to 7 days in the future. Note that the clocks can start earlier than specified, if players start making moves in the game. + If omitted, the clocks will not start automatically. + :param rated: set to true to make the games rated. defaults to false. + :param variant: variant of the games + :param fen: custom initial position (in FEN). Only allowed if variant is standard, fromPosition, or chess960 (if a valid 960 starting position), and the games cannot be rated. + :param message: message sent to players when their game is created + :param rules: extra game rules + :return: the newly created bulk pairing + """ + path = "/api/bulk-pairing" + payload = { + "players": ",".join(":".join(pair) for pair in token_pairings), + "clock.limit": clock_limit, + "clock.increment": clock_increment, + "days": days, + "pairAt": pair_at, + "startClocksAt": start_clocks_at, + "rated": rated, + "variant": variant, + "fen": fen, + "message": message, + "rules": ",".join(rules) if rules else None, + } + return cast( + BulkPairing, + self._r.post( + path, + payload=payload, + fmt=JSON, + ), + ) + + def start_clocks(self, bulk_pairing_id: str) -> None: + """Immediately start all clocks of the games of the given bulk pairing. + + This overrides the startClocksAt value of an existing bulk pairing. + + If the games have not yet been created (pairAt is in the future) or the clocks + have already started (startClocksAt is in the past), then this does nothing. + + :param bulk_pairing_id: id of the bulk pairing to start clocks of + """ + path = f"/api/bulk-pairing/{bulk_pairing_id}/start-clocks" + self._r.post(path) + + def cancel(self, bulk_pairing_id: str) -> None: + """Cancel and delete a bulk pairing that is scheduled in the future. + + If the games have already been created, then this does nothing. + + :param bulk_pairing_id: id of the bulk pairing to cancel + """ + path = f"/api/bulk-pairing/{bulk_pairing_id}" + self._r.request("DELETE", path) diff --git a/berserk/types/__init__.py b/berserk/types/__init__.py index 746064e..79047f0 100644 --- a/berserk/types/__init__.py +++ b/berserk/types/__init__.py @@ -1,18 +1,29 @@ from __future__ import annotations -from .team import Team, PaginatedTeams +from .account import AccountInformation, Perf, Preferences, Profile, StreamerInfo +from .bulk_pairings import BulkPairing, BulkPairingGame +from .common import ClockConfig from .opening_explorer import ( - OpeningStatistic, + OpeningExplorerRating, OpeningExplorerVariant, + OpeningStatistic, Speed, - OpeningExplorerRating, ) +from .team import PaginatedTeams, Team __all__ = [ - "Team", - "PaginatedTeams", - "OpeningStatistic", + "AccountInformation", + "BulkPairing", + "BulkPairingGame", + "ClockConfig", + "OpeningExplorerRating", "OpeningExplorerVariant", + "OpeningStatistic", + "PaginatedTeams", + "Perf", + "Preferences", + "Profile", "Speed", - "OpeningExplorerRating", + "StreamerInfo", + "Team", ] diff --git a/berserk/types/account.py b/berserk/types/account.py new file mode 100644 index 0000000..d6da3ee --- /dev/null +++ b/berserk/types/account.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from datetime import datetime + +from typing_extensions import TypedDict + + +class Perf(TypedDict): + games: int + rating: int + rd: int + prog: int + prov: bool + + +class Profile(TypedDict): + """Public profile of an account.""" + + country: str + location: str + bio: str + firstName: str + lastName: str + fideRating: int + uscfRating: int + ecfRating: int + links: str + + +class StreamerInfo(TypedDict): + """Information about the streamer on a specific platform.""" + + channel: str + + +class AccountInformation(TypedDict): + """Information about an account.""" + + id: str + username: str + perfs: dict[str, Perf] + createdAt: datetime + disabled: bool + tosViolation: bool + profile: Profile + seenAt: datetime + patron: bool + verified: bool + title: str + url: str + playing: str + count: dict[str, int] + streaming: bool + streamer: dict[str, StreamerInfo] + followable: bool + following: bool + blocking: bool + followsYou: bool + + +class Preferences(TypedDict, total=False): + dark: bool + transp: bool + bgImg: str + is3d: bool + theme: str + pieceSet: str + theme3d: str + pieceSet3d: str + soundSet: str + blindfold: int + autoQueen: int + autoThreefold: int + takeback: int + moretime: int + clockTenths: int + clockBar: bool + clockSound: bool + premove: bool + animation: int + captured: bool + follow: bool + highlight: bool + destination: bool + coords: int + replay: int + challenge: int + message: int + coordColor: int + submitMove: int + confirmResign: int + insightShare: int + keyboardMove: int + zen: int + moveEvent: int + rookCastle: int diff --git a/berserk/types/bulk_pairings.py b/berserk/types/bulk_pairings.py new file mode 100644 index 0000000..ad71cf0 --- /dev/null +++ b/berserk/types/bulk_pairings.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing_extensions import TypedDict + +from .common import ClockConfig, Variant + + +class BulkPairingGame(TypedDict): + id: str + black: str + white: str + + +class BulkPairing(TypedDict): + id: str + games: list[BulkPairingGame] + variant: Variant + clock: ClockConfig + pairAt: int + pairedAt: int | None + rated: bool + startClocksAt: int + scheduledAt: int diff --git a/berserk/types/common.py b/berserk/types/common.py new file mode 100644 index 0000000..c0709d5 --- /dev/null +++ b/berserk/types/common.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict, TypeAlias + + +class ClockConfig(TypedDict): + # starting time in seconds + limit: int + # increment in seconds + increment: int + + +Variant: TypeAlias = Literal[ + "standard", + "chess960", + "kingOfTheHill", + "threeCheck", + "antichess", + "atomic", + "horde", + "racingKings", + "crazyhouse", + "fromPosition", +] + +GameRule: TypeAlias = Literal[ + "noAbort", "noRematch", "noGiveTime", "noClaimWin", "noEarlyDraw" +]