{"id":50805509,"url":"https://github.com/obazin/litchee","last_synced_at":"2026-06-13T00:33:11.802Z","repository":{"id":363825002,"uuid":"1262315513","full_name":"obazin/litchee","owner":"obazin","description":"An async, builder-pattern Rust client for the Lichess API with PKCE OAuth support","archived":false,"fork":false,"pushed_at":"2026-06-10T13:14:20.000Z","size":147,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-10T15:10:44.784Z","etag":null,"topics":["api-client","async","builder-pattern","chess","lichess","lichess-api","oauth2","pkce","reqwest","rust","tokio"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/obazin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-07T21:05:40.000Z","updated_at":"2026-06-10T13:15:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/obazin/litchee","commit_stats":null,"previous_names":["obazin/litchee"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/obazin/litchee","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obazin%2Flitchee","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obazin%2Flitchee/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obazin%2Flitchee/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obazin%2Flitchee/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/obazin","download_url":"https://codeload.github.com/obazin/litchee/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obazin%2Flitchee/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34268187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","async","builder-pattern","chess","lichess","lichess-api","oauth2","pkce","reqwest","rust","tokio"],"created_at":"2026-06-13T00:33:10.046Z","updated_at":"2026-06-13T00:33:11.775Z","avatar_url":"https://github.com/obazin.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# litchee\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Rust 2024](https://img.shields.io/badge/rust-2024%20edition-orange.svg)](https://www.rust-lang.org)\n[![Lichess API](https://img.shields.io/badge/Lichess%20API-184%2F184%20operations-brightgreen.svg)](https://lichess.org/api)\n\n**`litchee`** is an asynchronous, builder-pattern Rust client for the\n[Lichess API], covering **every documented operation** (184/184 of the official\nOpenAPI spec) — from looking up players to playing games, running tournaments,\nstreaming live broadcasts, and \"Log in with Lichess\" via OAuth2 + PKCE.\n\nIt is open source (MIT) and aims at **feature parity** with the official API,\nwith an ergonomic, strongly-typed surface: every DTO is prefixed `Lichess*`,\nevery failure maps to a specific [`LichessError`] variant, and every endpoint is\nreached through a small accessor on the client (`client.account()`,\n`client.broadcasts()`, …).\n\n```rust,no_run\nuse futures_util::StreamExt;\nuse litchee::LichessClient;\n\n#[tokio::main]\nasync fn main() -\u003e litchee::Result\u003c()\u003e {\n    let client = LichessClient::builder().token(\"lip_your_token\").build()?;\n\n    // A simple JSON request.\n    let me = client.account().profile().await?;\n    println!(\"Logged in as {}\", me.user.username);\n\n    // A streaming (NDJSON) request.\n    let mut games = client.games().export_user(\"bobby\").max(5).stream().await?;\n    while let Some(game) = games.next().await {\n        println!(\"game {}\", game?.id);\n    }\n    Ok(())\n}\n```\n\n---\n\n## Why litchee?\n\nThe Lichess API is large (24 tags, ~190 operations, four hosts, JSON + NDJSON +\nPGN). `litchee` wraps all of it behind one cohesive, async-first client so Rust\napplications — bots, analysis tools, \"Log in with Lichess\" web apps, dataset\nexporters — can talk to Lichess without hand-rolling HTTP, streaming, and OAuth.\n\n- **Complete.** All 184 documented operations are implemented and covered by\n  tests.\n- **Async \u0026 streaming-native.** Built on `tokio` + `reqwest`; NDJSON endpoints\n  (event streams, board/bot game state, game exports) return a `Stream` you can\n  consume directly.\n- **Typed end to end.** `Lichess*` DTOs and an exhaustive, matchable error type.\n- **\"Log in with Lichess\".** First-class OAuth2 Authorization Code flow with\n  PKCE, plus plain personal access tokens.\n- **Organized by business concern.** The module tree mirrors the API's own\n  structure, grouped into categories under `litchee::api`.\n\n## Installation\n\n```toml\n[dependencies]\nlitchee = \"0.1\"\ntokio = { version = \"1\", features = [\"full\"] }\nfutures-util = \"0.3\" # to consume streams with `.next()`\n```\n\nThe minimum supported Rust version is **1.85** (edition 2024).\n\n## Examples\n\n### 1. Log in with Lichess (OAuth2 + PKCE)\n\n\u003e New to OAuth or PKCE? The [**PKCE flow guide**](PKCE_GUIDE.md) walks through the\n\u003e whole \"Log in with Lichess\" flow step by step, for beginners — with a glossary\n\u003e of OAuth terms at the end.\n\n```rust,no_run\nuse litchee::LichessClient;\nuse litchee::api::auth::oauth::{AuthorizationRequest, CodeExchange, Scope};\n\n# async fn run() -\u003e litchee::Result\u003c()\u003e {\nlet client = LichessClient::new();\n\n// 1. Build the authorization URL; persist `state` and `verifier` in the session.\nlet auth = client.oauth().authorization_url(\u0026AuthorizationRequest {\n    client_id: \"your.app\",\n    redirect_uri: \"https://your.app/callback\",\n    scopes: \u0026[Scope::PreferenceRead, Scope::PuzzleRead, Scope::StudyRead],\n    username_hint: None,\n})?;\nprintln!(\"Send the user to: {}\", auth.url);\n\n// 2. After the redirect, check `state`, then exchange the returned `code`.\nlet token = client.oauth().exchange_code(\u0026CodeExchange {\n    code: \"code_from_redirect\",\n    code_verifier: \u0026auth.verifier,\n    redirect_uri: \"https://your.app/callback\",\n    client_id: \"your.app\",\n}).await?;\n\n// 3. Build an authenticated client with the new token.\nlet user = LichessClient::builder().token(token.access_token).build()?;\nprintln!(\"Hello, {}\", user.account().profile().await?.user.username);\n# Ok(())\n# }\n```\n\n### 2. Export an authenticated user's games (NDJSON stream)\n\n```rust,no_run\nuse futures_util::StreamExt;\nuse litchee::LichessClient;\n\n# async fn run() -\u003e litchee::Result\u003c()\u003e {\nlet client = LichessClient::builder().token(\"lip_your_token\").build()?;\nlet me = client.account().profile().await?;\n\n// Stream this user's last 20 rated blitz games as decoded JSON.\nlet mut games = client\n    .games()\n    .export_user(\u0026me.user.username)\n    .max(20)\n    .rated(true)\n    .perf_type(\"blitz\")\n    .stream()\n    .await?;\n\nwhile let Some(game) = games.next().await {\n    let game = game?;\n    println!(\"{} — winner: {:?}\", game.id, game.winner);\n}\n# Ok(())\n# }\n```\n\n### 3. A user's played puzzles\n\n```rust,no_run\nuse futures_util::StreamExt;\nuse litchee::LichessClient;\n\n# async fn run() -\u003e litchee::Result\u003c()\u003e {\nlet client = LichessClient::builder().token(\"lip_your_token\").build()?;\n\n// Stream the authenticated user's puzzle history (needs the `puzzle:read` scope).\nlet mut activity = client.puzzles().activity(Some(50)).await?;\nwhile let Some(round) = activity.next().await {\n    let round = round?;\n    let outcome = if round.win { \"solved\" } else { \"failed\" };\n    println!(\"puzzle {} — {outcome}\", round.puzzle.id);\n}\n# Ok(())\n# }\n```\n\n### 4. Studies (list + export PGN)\n\n```rust,no_run\nuse futures_util::StreamExt;\nuse litchee::LichessClient;\n\n# async fn run() -\u003e litchee::Result\u003c()\u003e {\nlet client = LichessClient::builder().token(\"lip_your_token\").build()?;\n\n// List a user's studies, then export the first one as PGN.\nlet mut studies = client.studies().list_metadata(\"bobby\").await?;\nif let Some(study) = studies.next().await {\n    let study = study?;\n    let pgn = client.studies().export_study_pgn(\u0026study.id).await?;\n    println!(\"{} — {} bytes of PGN\", study.name, pgn.len());\n}\n# Ok(())\n# }\n```\n\n### 5. Broadcasting (browse + round PGN)\n\n```rust,no_run\nuse futures_util::StreamExt;\nuse litchee::LichessClient;\n\n# async fn run() -\u003e litchee::Result\u003c()\u003e {\nlet client = LichessClient::new();\n\n// Browse official broadcasts, then export a round's games as PGN.\nlet mut official = client.broadcasts().official().await?;\nif let Some(broadcast) = official.next().await {\n    let broadcast = broadcast?;\n    println!(\"Broadcast: {}\", broadcast.tour.name);\n    if let Some(round) = broadcast.rounds.first() {\n        let pgn = client.broadcasts().round_pgn(\u0026round.id).await?;\n        println!(\"Round '{}' — {} bytes of PGN\", round.name, pgn.len());\n    }\n}\n# Ok(())\n# }\n```\n\nThe [`examples/`](examples/) directory contains runnable programs\n(`cargo run --example profile`, `cargo run --example tv_feed`).\n\n## API coverage\n\nEvery documented Lichess operation is implemented. Endpoints are reached through\nan accessor method on `LichessClient` and live in a module under `litchee::api`,\ngrouped by category. The tables below map each concern's endpoints to its module.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eAuth\u003c/b\u003e\u003c/summary\u003e\n\n**`client.oauth()`** — `litchee::api::auth::oauth`  (4 endpoints)\n\n`GET /oauth`, `POST /api/token`, `DELETE /api/token`, `POST /api/token/test`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eUsers\u003c/b\u003e\u003c/summary\u003e\n\n**`client.account()`** — `litchee::api::users::account`  (6 endpoints)\n\n`GET /api/account`, `GET /api/account/email`, `GET /api/account/preferences`, `GET /api/account/kid`, `POST /api/account/kid`, `GET /api/timeline`\n\n**`client.users()`** — `litchee::api::users::players`  (13 endpoints)\n\n`GET /api/user/{username}`, `POST /api/users`, `GET /api/users/status`, `GET /api/crosstable/{u1}/{u2}`, `GET /api/player/autocomplete`, `GET /api/user/{username}/rating-history`, `GET /api/user/{username}/perf/{perf}`, `GET /api/user/{username}/activity`, `GET /api/player`, `GET /api/player/top/{nb}/{perfType}`, `GET /api/streamer/live`, `GET /api/user/{username}/note`, `POST /api/user/{username}/note`\n\n**`client.fide()`** — `litchee::api::users::fide`  (3 endpoints)\n\n`GET /api/fide/player/{playerId}`, `GET /api/fide/player/{playerId}/ratings`, `GET /api/fide/player`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eSocial\u003c/b\u003e\u003c/summary\u003e\n\n**`client.relations()`** — `litchee::api::social::relations`  (5 endpoints)\n\n`GET /api/rel/following`, `POST /api/rel/follow/{username}`, `POST /api/rel/unfollow/{username}`, `POST /api/rel/block/{username}`, `POST /api/rel/unblock/{username}`\n\n**`client.messaging()`** — `litchee::api::social::messaging`  (1 endpoint)\n\n`POST /inbox/{username}`\n\n**`client.teams()`** — `litchee::api::social::teams`  (14 endpoints)\n\n`GET /api/team/{teamId}`, `GET /api/team/all`, `GET /api/team/of/{username}`, `GET /api/team/search`, `GET /api/team/{teamId}/users`, `GET /api/team/{teamId}/requests`, `POST /api/team/{teamId}/request/{userId}/accept`, `POST /api/team/{teamId}/request/{userId}/decline`, `POST /api/team/{teamId}/kick/{userId}`, `POST /team/{teamId}/join`, `POST /team/{teamId}/quit`, `POST /team/{teamId}/pm-all`, `GET /api/team/{teamId}/arena`, `GET /api/team/{teamId}/swiss`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eTournaments\u003c/b\u003e\u003c/summary\u003e\n\n**`client.arena()`** — `litchee::api::tournaments::arena`  (13 endpoints)\n\n`GET /api/tournament`, `GET /api/tournament/{id}`, `POST /api/tournament`, `POST /api/tournament/{id}`, `POST /api/tournament/team-battle/{id}`, `GET /api/tournament/{id}/teams`, `POST /api/tournament/{id}/join`, `POST /api/tournament/{id}/withdraw`, `POST /api/tournament/{id}/terminate`, `GET /api/tournament/{id}/results`, `GET /api/tournament/{id}/games`, `GET /api/user/{username}/tournament/created`, `GET /api/user/{username}/tournament/played`\n\n**`client.swiss()`** — `litchee::api::tournaments::swiss`  (10 endpoints)\n\n`GET /api/swiss/{id}`, `POST /api/swiss/new/{teamId}`, `POST /api/swiss/{id}/edit`, `POST /api/swiss/{id}/join`, `POST /api/swiss/{id}/withdraw`, `POST /api/swiss/{id}/terminate`, `POST /api/swiss/{id}/schedule-next-round`, `GET /swiss/{id}.trf`, `GET /api/swiss/{id}/results`, `GET /api/swiss/{id}/games`\n\n**`client.simuls()`** — `litchee::api::tournaments::simuls`  (1 endpoint)\n\n`GET /api/simul`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eTraining\u003c/b\u003e\u003c/summary\u003e\n\n**`client.puzzles()`** — `litchee::api::training::puzzles`  (11 endpoints)\n\n`GET /api/puzzle/daily`, `GET /api/puzzle/{id}`, `GET /api/puzzle/next`, `GET /api/puzzle/activity`, `GET /api/puzzle/batch/{angle}`, `POST /api/puzzle/batch/{angle}`, `GET /api/puzzle/dashboard/{days}`, `GET /api/puzzle/replay/{days}/{theme}`, `GET /api/storm/dashboard/{username}`, `GET /api/racer/{id}`, `POST /api/racer`\n\n**`client.studies()`** — `litchee::api::training::studies`  (9 endpoints)\n\n`GET /api/study/{studyId}/{chapterId}.pgn`, `GET /api/study/{studyId}.pgn`, `GET /api/study/by/{username}/export.pgn`, `GET /api/study/by/{username}`, `POST /api/study`, `POST /api/study/{studyId}/import-pgn`, `POST /api/study/{studyId}/{chapterId}/moves`, `POST /api/study/{studyId}/{chapterId}/tags`, `DELETE /api/study/{studyId}/{chapterId}`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eBroadcasting\u003c/b\u003e\u003c/summary\u003e\n\n**`client.broadcasts()`** — `litchee::api::broadcasting::broadcasts`  (19 endpoints)\n\n`GET /api/broadcast`, `GET /api/broadcast/top`, `GET /api/broadcast/search`, `GET /api/broadcast/by/{username}`, `GET /api/broadcast/my-rounds`, `GET /api/broadcast/{id}`, `GET /api/broadcast/{tourSlug}/{roundSlug}/{roundId}`, `GET /api/broadcast/round/{roundId}.pgn`, `GET /api/broadcast/{id}.pgn`, `GET /api/stream/broadcast/round/{roundId}.pgn`, `POST /api/broadcast/round/{roundId}/push`, `POST /api/broadcast/round/{roundId}/reset`, `GET /broadcast/{id}/players`, `GET /broadcast/{id}/players/{playerId}`, `GET /broadcast/{id}/teams/standings`, `POST /broadcast/new`, `POST /broadcast/{id}/edit`, `POST /broadcast/{id}/new`, `POST /broadcast/round/{roundId}/edit`\n\n**`client.tv()`** — `litchee::api::broadcasting::tv`  (4 endpoints)\n\n`GET /api/tv/channels`, `GET /api/tv/feed`, `GET /api/tv/{channel}`, `GET /api/tv/{channel}/feed`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eDatabase\u003c/b\u003e (separate hosts: explorer / tablebase)\u003c/summary\u003e\n\n**`client.opening_explorer()`** — `litchee::api::database::opening_explorer`  (4 endpoints, `explorer.lichess.org`)\n\n`GET /masters`, `GET /lichess`, `GET /player`, `GET /masters/pgn/{gameId}`\n\n**`client.tablebase()`** — `litchee::api::database::tablebase`  (3 endpoints, `tablebase.lichess.org`)\n\n`GET /standard`, `GET /atomic`, `GET /antichess`\n\n**`client.analysis()`** — `litchee::api::database::analysis`  (1 endpoint)\n\n`GET /api/cloud-eval`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eGameplay\u003c/b\u003e\u003c/summary\u003e\n\n**`client.board()`** — `litchee::api::gameplay::board`  (13 endpoints)\n\n`GET /api/stream/event`, `GET /api/board/game/stream/{gameId}`, `POST /api/board/game/{gameId}/move/{move}`, `POST /api/board/game/{gameId}/abort`, `POST /api/board/game/{gameId}/resign`, `POST /api/board/game/{gameId}/draw/{accept}`, `POST /api/board/game/{gameId}/takeback/{accept}`, `POST /api/board/game/{gameId}/claim-victory`, `POST /api/board/game/{gameId}/claim-draw`, `POST /api/board/game/{gameId}/berserk`, `GET /api/board/game/{gameId}/chat`, `POST /api/board/game/{gameId}/chat`, `POST /api/board/seek`\n\n**`client.bot()`** — `litchee::api::gameplay::bot`  (13 endpoints)\n\n`POST /api/bot/account/upgrade`, `GET /api/bot/online`, `GET /api/stream/event`, `GET /api/bot/game/stream/{gameId}`, `POST /api/bot/game/{gameId}/move/{move}`, `POST /api/bot/game/{gameId}/abort`, `POST /api/bot/game/{gameId}/resign`, `POST /api/bot/game/{gameId}/draw/{accept}`, `POST /api/bot/game/{gameId}/takeback/{accept}`, `POST /api/bot/game/{gameId}/claim-victory`, `POST /api/bot/game/{gameId}/claim-draw`, `GET /api/bot/game/{gameId}/chat`, `POST /api/bot/game/{gameId}/chat`\n\n**`client.challenges()`** — `litchee::api::gameplay::challenges`  (11 endpoints)\n\n`GET /api/challenge`, `GET /api/challenge/{challengeId}/show`, `POST /api/challenge/{username}`, `POST /api/challenge/ai`, `POST /api/challenge/open`, `POST /api/challenge/{challengeId}/accept`, `POST /api/challenge/{challengeId}/decline`, `POST /api/challenge/{challengeId}/cancel`, `POST /api/challenge/{gameId}/start-clocks`, `POST /api/round/{gameId}/add-time/{seconds}`, `POST /api/token/admin-challenge`\n\n**`client.bulk_pairing()`** — `litchee::api::gameplay::bulk_pairing`  (6 endpoints)\n\n`GET /api/bulk-pairing`, `POST /api/bulk-pairing`, `GET /api/bulk-pairing/{id}`, `DELETE /api/bulk-pairing/{id}`, `POST /api/bulk-pairing/{id}/start-clocks`, `GET /api/bulk-pairing/{id}/games`\n\n**`client.games()`** — `litchee::api::gameplay::games`  (13 endpoints)\n\n`GET /game/export/{gameId}`, `GET /api/games/user/{username}`, `POST /api/games/export/_ids`, `GET /api/games/export/bookmarks`, `GET /api/games/export/imports`, `GET /api/account/playing`, `GET /api/user/{username}/current-game`, `GET /game/{gameId}/chat`, `POST /api/import`, `GET /api/stream/game/{id}`, `POST /api/stream/games-by-users`, `POST /api/stream/games/{streamId}`, `POST /api/stream/games/{streamId}/add`\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eEngine\u003c/b\u003e (work endpoints on `engine.lichess.ovh`)\u003c/summary\u003e\n\n**`client.external_engine()`** — `litchee::api::engine::external_engine`  (8 endpoints)\n\n`GET /api/external-engine`, `POST /api/external-engine`, `GET /api/external-engine/{id}`, `PUT /api/external-engine/{id}`, `DELETE /api/external-engine/{id}`, `POST /api/external-engine/{id}/analyse`, `POST /api/external-engine/work`, `POST /api/external-engine/work/{id}`\n\n\u003c/details\u003e\n\n\u003e The OAuth `GET /oauth` endpoint is not a request the client makes — it's the\n\u003e URL you redirect the user's browser to. `client.oauth().authorization_url(…)`\n\u003e builds it. `GET /api/stream/event` is shared by the Board and Bot APIs.\n\n## Design \u0026 technical choices\n\n- **Async-first on `tokio` + `reqwest` (rustls).** Async is required because many\n  Lichess endpoints stream newline-delimited JSON (`application/x-ndjson`):\n  event streams, board/bot game state, game/tournament exports, TV feeds.\n- **Ergonomic streaming.** Streaming endpoints return\n  `BoxStream\u003c'static, Result\u003cT\u003e\u003e` — `Unpin`, `Send`, and consumable directly with\n  `StreamExt::next()`. Lines are buffered across network chunks and keep-alive\n  blanks are skipped.\n- **Exhaustive, matchable errors.** Every failure maps to a specific\n  [`LichessError`] variant: a structured `ApiError` (status → typed kind + body\n  message + `Retry-After`), a typed `OAuthError`, transport/decode/stream\n  failures, and PKCE validation errors.\n- **Builder pattern.** The client (`LichessClient::builder()`) and every request\n  with optional parameters (game export, challenges, tournaments, …) use\n  builders rather than wide function signatures.\n- **Four hosts, one client.** `lichess.org`, `explorer.lichess.org`,\n  `tablebase.lichess.org`, and `engine.lichess.ovh` are routed internally; each\n  is overridable on the builder (for self-hosted lila, `localhost`, or mocks).\n- **Strong, forward-compatible types.** Every DTO is prefixed `Lichess*` and is\n  `#[non_exhaustive]`. Where the API returns very large or evolving aggregates\n  (e.g. perf stats, activity feeds, broadcast nested payloads), the documented\n  fields are typed and the remainder is preserved losslessly in `serde_json`\n  values — nothing is dropped.\n- **Organized by business concern.** The module tree mirrors the API's own\n  organization, grouped into categories under `src/api/` (see below). Core\n  plumbing (`client`, `config`, `error`, `http`, `model`, `stream`) lives at the\n  crate root.\n- **Tested deterministically.** Each endpoint has an integration test that runs\n  against a [`wiremock`](https://docs.rs/wiremock) mock server with fixtures\n  derived from the spec's own examples; pure logic (PKCE derivation, NDJSON\n  splitting, error mapping, serde round-trips) has unit tests. CI runs\n  `fmt`, `clippy -D warnings`, the test suite, the doc build, and an MSRV check.\n- **Safety \u0026 quality gates.** `#![forbid(unsafe_code)]`, clippy `pedantic`,\n  `missing_docs`, and a self-imposed ≤600 LOC/file and ≤20 LOC/method limit.\n\n### Project layout\n\n```\nsrc/\n  lib.rs\n  client/ config/ error/ http/ model/ stream/   # core plumbing\n  api/\n    auth/          oauth\n    users/         account, players, fide\n    social/        relations, messaging, teams\n    tournaments/   arena, swiss, simuls\n    training/      puzzles, studies\n    broadcasting/  broadcasts, tv\n    database/      opening_explorer, tablebase, analysis\n    gameplay/      board, bot, challenges, bulk_pairing, games\n    engine/        external_engine\n```\n\n## The vendored OpenAPI spec (`reference/` submodule)\n\nThis repository includes the **official Lichess OpenAPI specification** as a git\nsubmodule at [`reference/lichess-api/`](reference/lichess-api) (source:\n[lichess-org/api](https://github.com/lichess-org/api)). It is the **source of\ntruth** for the client and is used during development to:\n\n- **Model DTOs faithfully** — field names, optionality, and enums are taken\n  directly from the spec's schemas.\n- **Drive deterministic tests** — integration-test fixtures are derived from the\n  spec's documented examples, so tests need no network or credentials.\n- **Verify coverage** — implemented endpoints are diffed against the spec to\n  guarantee full (184/184) coverage as the API evolves.\n\nThe submodule is **development-only**: it is excluded from the published crate.\nClone it with:\n\n```bash\ngit clone --recurse-submodules https://github.com/obazin/litchee\n# or, in an existing clone:\ngit submodule update --init --recursive\n```\n\n## Development\n\n```bash\ncargo build\ncargo test                                       # unit + integration tests\ncargo clippy --all-targets --all-features -- -D warnings\ncargo fmt\ncargo doc --no-deps --open\n```\n\n## Contributing\n\nContributions are welcome. Please keep the project conventions: one concern per\nmodule under the right `src/api/` category, `Lichess*`-prefixed DTOs, an\nintegration test per endpoint plus unit tests for pure functions, and a clean\n`cargo fmt` / `cargo clippy -D warnings`. The vendored spec under `reference/` is\nthe source of truth for endpoints and types.\n\n## License\n\nLicensed under the [MIT License](LICENSE).\n\n[Lichess API]: https://lichess.org/api\n[`LichessError`]: https://docs.rs/litchee/latest/litchee/error/enum.LichessError.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobazin%2Flitchee","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fobazin%2Flitchee","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobazin%2Flitchee/lists"}