pwenker commited on
Commit
1b2d9aa
·
1 Parent(s): 6599e63
README.md CHANGED
@@ -11,7 +11,7 @@ pinned: True
11
 
12
  # Welcome to Chessli2 🏰
13
 
14
- Chessli2 is your **always free** and **open-source** chess trainer 🛡️, designed to elevate your game by allowing you to analyze games, identify mistakes, and sharpen your tactics, all sourced directly from [lichess.org](https://lichess.org/).
15
 
16
  ## Why a second version?
17
 
@@ -27,10 +27,50 @@ This overwhelming support has inspired me to develop a sleek new version of Ches
27
 
28
  Chessli2 is here to support your journey to becoming a chess master. Dive in and start enhancing your skills today! 🚀
29
 
30
- ## Quickstart
31
 
32
- Try it directly out at [Huggingface Spaces](https://pwenker-chessli2.hf.space/).
 
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  ## FAQ
36
 
 
11
 
12
  # Welcome to Chessli2 🏰
13
 
14
+ [Chessli2](https://github.com/pwenker/chessli2) is your **always free** and **open-source** chess trainer 🛡️, designed to elevate your game by allowing you to analyze games, identify mistakes, and sharpen your tactics, all sourced directly from [lichess.org](https://lichess.org/).
15
 
16
  ## Why a second version?
17
 
 
27
 
28
  Chessli2 is here to support your journey to becoming a chess master. Dive in and start enhancing your skills today! 🚀
29
 
30
+ ## Quickstart 🚀
31
 
32
+ ### 👉 Click here to try out the app directly:
33
+ [**Chessli2**](https://pwenker-chessli2.hf.space/)
34
 
35
+ ### 🔍 Inspect code at:
36
+ - **GitHub:** [pwenker/chessli2](https://github.com/pwenker/chessli2)
37
+ - **Hugging Face Spaces:** [pwenker/chessli2](https://huggingface.co/spaces/pwenker/chessli2)
38
+
39
+
40
+ ## Local Deployment 🏠
41
+
42
+ ### Prerequisites 📋
43
+
44
+ #### Rye 🌾
45
+ [Install `Rye`](https://rye-up.com/guide/installation/#installing-rye)
46
+ > Rye is a comprehensive tool designed for Python developers. It simplifies your workflow by managing Python installations and dependencies. Simply install Rye, and it takes care of the rest.
47
+
48
+ ### Set-Up 🛠️
49
+
50
+ Clone the repository, e.g. with:
51
+ ```
52
+ git clone https://github.com/pwenker/chessli2.git
53
+ ```
54
+ Navigate to the directory:
55
+ ```
56
+ cd chessli2
57
+ ```
58
+ And execute:
59
+ ```
60
+ rye sync
61
+ ```
62
+ This creates a virtual environment in `.venv` and synchronizes the repo.
63
+
64
+ For more details, visit: [Basics - Rye](https://rye-up.com/guide/basics/)
65
+
66
+ ### Start the App 🌟
67
+
68
+ Launch the app using:
69
+ ```
70
+ rye run python src/chessli2/app.py
71
+ ```
72
+
73
+ Finally, open your browser and visit [http://localhost:7860](http://localhost:7860/) to start practicing!
74
 
75
  ## FAQ
76
 
app.py DELETED
@@ -1,5 +0,0 @@
1
- from src.chessli2.gui import demo
2
-
3
-
4
- if __name__ == "__main__":
5
- demo.launch(show_error=True)
 
 
 
 
 
 
docs/puzzle_themes.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ | Name | Description |
2
+ |-------------------------|---------------------------------------------------------------------------------------------------------------------|
3
+ | Advanced pawn | One of your pawns is deep into the opponent position, maybe threatening to promote. |
4
+ | Advantage | Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp) |
5
+ | Anastasia's mate | A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece. |
6
+ | Arabian mate | A knight and a rook team up to trap the opposing king on a corner of the board. |
7
+ | Attacking f2 or f7 | An attack focusing on the f2 or f7 pawn, such as in the fried liver opening. |
8
+ | Attraction | An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic. |
9
+ | Back rank mate | Checkmate the king on the home rank, when it is trapped there by its own pieces. |
10
+ | Bishop endgame | An endgame with only bishops and pawns. |
11
+ | Boden's mate | Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces. |
12
+ | Castling | Bring the king to safety, and deploy the rook for attack. |
13
+ | Capture the defender | Removing a piece that is critical to defence of another piece, allowing the now undefended piece to be captured on a following move. |
14
+ | Crushing | Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp) |
15
+ | Double bishop mate | Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces. |
16
+ | Dovetail mate | A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces. |
17
+ | Equality | Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp) |
18
+ | Kingside attack | An attack of the opponent's king, after they castled on the king side. |
19
+ | Clearance | A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea. |
20
+ | Defensive move | A precise move or sequence of moves that is needed to avoid losing material or another advantage. |
21
+ | Deflection | A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called "overloading". |
22
+ | Discovered attack | Moving a piece (such as a knight), that previously blocked an attack by a long range piece (such as a rook), out of the way of that piece. |
23
+ | Double check | Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent's king. |
24
+ | Endgame | A tactic during the last phase of the game. |
25
+ | En passant | A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move. |
26
+ | Exposed king | A tactic involving a king with few defenders around it, often leading to checkmate. |
27
+ | Fork | A move where the moved piece attacks two opponent pieces at once. |
28
+ | Hanging piece | A tactic involving an opponent piece being undefended or insufficiently defended and free to capture. |
29
+ | Hook mate | Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king's escape. |
30
+ | Interference | Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks. |
31
+ | Intermezzo | Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as "Zwischenzug" or "In between". |
32
+ | Knight endgame | An endgame with only knights and pawns. |
33
+ | Long | Three moves to win. |
34
+ | Master games | Puzzles from games played by titled players. |
35
+ | Master vs Master games | Puzzles from games between two titled players. |
36
+ | Checkmate | Win the game with style. |
37
+ | Mate in 1 | Deliver checkmate in one move. |
38
+ | Mate in 2 | Deliver checkmate in two moves. |
39
+ | Mate in 3 | Deliver checkmate in three moves. |
40
+ | Mate in 4 | Deliver checkmate in four moves. |
41
+ | Mate in 5 or more | Figure out a long mating sequence. |
42
+ | Middlegame | A tactic during the second phase of the game. |
43
+ | One-move puzzle | A puzzle that is only one move long. |
44
+ | Opening | A tactic during the first phase of the game. |
45
+ | Pawn endgame | An endgame with only pawns. |
46
+ | Pin | A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece. |
47
+ | Promotion | Promote one of your pawn to a queen or minor piece. |
48
+ | Queen endgame | An endgame with only queens and pawns. |
49
+ | Queen and Rook | An endgame with only queens, rooks and pawns. |
50
+ | Queenside attack | An attack of the opponent's king, after they castled on the queen side. |
51
+ | Quiet move | A move that does neither make a check or capture, nor an immediate threat to capture, but does prepare a more hidden unavoidable threat for a later move. |
52
+ | Rook endgame | An endgame with only rooks and pawns. |
53
+ | Sacrifice | A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves. |
54
+ | Short | Two moves to win. |
55
+ | Skewer | A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin. |
56
+ | Smothered mate | A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces. |
57
+ | Super GM games | Puzzles from games played by the best players in the world. |
58
+ | Trapped piece | A piece is unable to escape capture as it has limited moves. |
59
+ | Underpromotion | Promotion to a knight, bishop, or rook. |
60
+ | Very long | Four moves or more to win. |
61
+ | X-Ray attack | A piece attacks or defends a square, through an enemy piece. |
62
+ | Zugzwang | The opponent is limited in the moves they can make, and all moves worsen their position. |
63
+ | Healthy mix | A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games. |
64
+ | Player games | Lookup puzzles generated from your games, or from another player's games. |
65
+ | Puzzle download information | These puzzles are in the public domain, and can be downloaded from %s. |
pyproject.toml CHANGED
@@ -13,12 +13,13 @@ dependencies = [
13
  "gradio_calendar>=0.0.4",
14
  "gradio>=4.29.0",
15
  "tabulate>=0.9.0",
 
16
  ]
17
  readme = "README.md"
18
- requires-python = ">= 3.10"
19
 
20
  [project.scripts]
21
- chessli2 = "chessli2.main:app"
22
 
23
  [build-system]
24
  requires = ["hatchling"]
@@ -26,7 +27,10 @@ build-backend = "hatchling.build"
26
 
27
  [tool.rye]
28
  managed = true
29
- dev-dependencies = []
 
 
 
30
 
31
  [tool.hatch.metadata]
32
  allow-direct-references = true
 
13
  "gradio_calendar>=0.0.4",
14
  "gradio>=4.29.0",
15
  "tabulate>=0.9.0",
16
+ "typer>=0.12.3",
17
  ]
18
  readme = "README.md"
19
+ requires-python = ">= 3.11"
20
 
21
  [project.scripts]
22
+ chessli2 = "chessli2.cli:app"
23
 
24
  [build-system]
25
  requires = ["hatchling"]
 
27
 
28
  [tool.rye]
29
  managed = true
30
+ dev-dependencies = [
31
+ "pytest>=8.2.0",
32
+ "pytest-sugar>=1.0.0",
33
+ ]
34
 
35
  [tool.hatch.metadata]
36
  allow-direct-references = true
requirements-dev.lock CHANGED
@@ -5,78 +5,233 @@
5
  # pre: false
6
  # features: []
7
  # all-features: false
 
8
 
9
  -e file:.
10
  aiofiles==23.2.1
11
- altair==5.2.0
 
 
12
  annotated-types==0.6.0
13
- anyio==4.2.0
 
 
 
 
14
  attrs==23.2.0
 
 
15
  berserk==0.13.2
16
- certifi==2023.11.17
 
 
 
 
17
  charset-normalizer==3.3.2
 
18
  chess==1.10.0
 
19
  click==8.1.7
 
 
 
 
20
  cycler==0.12.1
 
21
  deprecated==1.2.14
22
- exceptiongroup==1.2.1
23
- fastapi==0.109.0
24
- ffmpy==0.3.1
25
- filelock==3.13.1
26
- fonttools==4.47.2
27
- fsspec==2023.12.2
28
- gradio==4.29.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  gradio-calendar==0.0.4
30
- gradio-client==0.16.1
 
 
31
  h11==0.14.0
32
- httpcore==1.0.2
33
- httpx==0.26.0
34
- huggingface-hub==0.20.2
35
- idna==3.6
36
- importlib-resources==6.1.1
37
- jinja2==3.1.3
38
- jsonschema==4.20.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  jsonschema-specifications==2023.12.1
 
40
  kiwisolver==1.4.5
 
41
  markdown-it-py==3.0.0
42
- markupsafe==2.1.3
43
- matplotlib==3.5.3
 
 
 
 
44
  mdurl==0.1.2
 
45
  ndjson==0.3.1
46
- numpy==1.24.4
47
- orjson==3.9.10
48
- packaging==23.2
49
- pandas==2.0.3
50
- pillow==10.2.0
51
- pydantic==2.5.3
52
- pydantic-core==2.14.6
53
- pydantic-settings==2.1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  pydub==0.25.1
55
- pygments==2.17.2
56
- pyparsing==3.1.1
57
- python-dateutil==2.8.2
58
- python-dotenv==1.0.0
 
 
 
 
 
 
 
 
 
 
 
59
  python-multipart==0.0.9
60
- pytz==2023.3.post1
 
 
 
61
  pyyaml==6.0.1
62
- referencing==0.32.1
 
 
 
 
 
63
  requests==2.31.0
64
- rich==13.7.0
65
- rpds-py==0.17.1
66
- ruff==0.4.3
 
 
 
 
 
 
67
  semantic-version==2.10.0
 
68
  shellingham==1.5.4
 
69
  six==1.16.0
70
- sniffio==1.3.0
71
- starlette==0.35.1
 
 
 
 
72
  tabulate==0.9.0
 
 
 
73
  tomlkit==0.12.0
74
- toolz==0.12.0
75
- tqdm==4.66.1
 
 
 
76
  typer==0.12.3
77
- typing-extensions==4.9.0
78
- tzdata==2023.4
79
- urllib3==2.1.0
80
- uvicorn==0.25.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  websockets==11.0.3
 
 
82
  wrapt==1.16.0
 
 
5
  # pre: false
6
  # features: []
7
  # all-features: false
8
+ # with-sources: false
9
 
10
  -e file:.
11
  aiofiles==23.2.1
12
+ # via gradio
13
+ altair==5.3.0
14
+ # via gradio
15
  annotated-types==0.6.0
16
+ # via pydantic
17
+ anyio==4.3.0
18
+ # via httpx
19
+ # via starlette
20
+ # via watchfiles
21
  attrs==23.2.0
22
+ # via jsonschema
23
+ # via referencing
24
  berserk==0.13.2
25
+ # via chessli2
26
+ certifi==2024.2.2
27
+ # via httpcore
28
+ # via httpx
29
+ # via requests
30
  charset-normalizer==3.3.2
31
+ # via requests
32
  chess==1.10.0
33
+ # via chessli2
34
  click==8.1.7
35
+ # via typer
36
+ # via uvicorn
37
+ contourpy==1.2.1
38
+ # via matplotlib
39
  cycler==0.12.1
40
+ # via matplotlib
41
  deprecated==1.2.14
42
+ # via berserk
43
+ dnspython==2.6.1
44
+ # via email-validator
45
+ email-validator==2.1.1
46
+ # via fastapi
47
+ fastapi==0.111.0
48
+ # via fastapi-cli
49
+ # via gradio
50
+ fastapi-cli==0.0.3
51
+ # via fastapi
52
+ ffmpy==0.3.2
53
+ # via gradio
54
+ filelock==3.14.0
55
+ # via huggingface-hub
56
+ fonttools==4.51.0
57
+ # via matplotlib
58
+ fsspec==2024.3.1
59
+ # via gradio-client
60
+ # via huggingface-hub
61
+ gradio==4.31.0
62
+ # via chessli2
63
+ # via gradio-calendar
64
  gradio-calendar==0.0.4
65
+ # via chessli2
66
+ gradio-client==0.16.2
67
+ # via gradio
68
  h11==0.14.0
69
+ # via httpcore
70
+ # via uvicorn
71
+ httpcore==1.0.5
72
+ # via httpx
73
+ httptools==0.6.1
74
+ # via uvicorn
75
+ httpx==0.27.0
76
+ # via fastapi
77
+ # via gradio
78
+ # via gradio-client
79
+ huggingface-hub==0.23.0
80
+ # via gradio
81
+ # via gradio-client
82
+ idna==3.7
83
+ # via anyio
84
+ # via email-validator
85
+ # via httpx
86
+ # via requests
87
+ importlib-resources==6.4.0
88
+ # via gradio
89
+ iniconfig==2.0.0
90
+ # via pytest
91
+ jinja2==3.1.4
92
+ # via altair
93
+ # via fastapi
94
+ # via gradio
95
+ jsonschema==4.22.0
96
+ # via altair
97
  jsonschema-specifications==2023.12.1
98
+ # via jsonschema
99
  kiwisolver==1.4.5
100
+ # via matplotlib
101
  markdown-it-py==3.0.0
102
+ # via rich
103
+ markupsafe==2.1.5
104
+ # via gradio
105
+ # via jinja2
106
+ matplotlib==3.8.4
107
+ # via gradio
108
  mdurl==0.1.2
109
+ # via markdown-it-py
110
  ndjson==0.3.1
111
+ # via berserk
112
+ numpy==1.26.4
113
+ # via altair
114
+ # via contourpy
115
+ # via gradio
116
+ # via matplotlib
117
+ # via pandas
118
+ orjson==3.10.3
119
+ # via fastapi
120
+ # via gradio
121
+ packaging==24.0
122
+ # via altair
123
+ # via gradio
124
+ # via gradio-client
125
+ # via huggingface-hub
126
+ # via matplotlib
127
+ # via pytest
128
+ # via pytest-sugar
129
+ pandas==2.2.2
130
+ # via altair
131
+ # via gradio
132
+ pillow==10.3.0
133
+ # via gradio
134
+ # via matplotlib
135
+ pluggy==1.5.0
136
+ # via pytest
137
+ pydantic==2.7.1
138
+ # via chessli2
139
+ # via fastapi
140
+ # via gradio
141
+ # via pydantic-settings
142
+ pydantic-core==2.18.2
143
+ # via pydantic
144
+ pydantic-settings==2.2.1
145
+ # via chessli2
146
  pydub==0.25.1
147
+ # via gradio
148
+ pygments==2.18.0
149
+ # via rich
150
+ pyparsing==3.1.2
151
+ # via matplotlib
152
+ pytest==8.2.0
153
+ # via pytest-sugar
154
+ pytest-sugar==1.0.0
155
+ python-dateutil==2.9.0.post0
156
+ # via berserk
157
+ # via matplotlib
158
+ # via pandas
159
+ python-dotenv==1.0.1
160
+ # via pydantic-settings
161
+ # via uvicorn
162
  python-multipart==0.0.9
163
+ # via fastapi
164
+ # via gradio
165
+ pytz==2024.1
166
+ # via pandas
167
  pyyaml==6.0.1
168
+ # via gradio
169
+ # via huggingface-hub
170
+ # via uvicorn
171
+ referencing==0.35.1
172
+ # via jsonschema
173
+ # via jsonschema-specifications
174
  requests==2.31.0
175
+ # via berserk
176
+ # via huggingface-hub
177
+ rich==13.7.1
178
+ # via typer
179
+ rpds-py==0.18.1
180
+ # via jsonschema
181
+ # via referencing
182
+ ruff==0.4.4
183
+ # via gradio
184
  semantic-version==2.10.0
185
+ # via gradio
186
  shellingham==1.5.4
187
+ # via typer
188
  six==1.16.0
189
+ # via python-dateutil
190
+ sniffio==1.3.1
191
+ # via anyio
192
+ # via httpx
193
+ starlette==0.37.2
194
+ # via fastapi
195
  tabulate==0.9.0
196
+ # via chessli2
197
+ termcolor==2.4.0
198
+ # via pytest-sugar
199
  tomlkit==0.12.0
200
+ # via gradio
201
+ toolz==0.12.1
202
+ # via altair
203
+ tqdm==4.66.4
204
+ # via huggingface-hub
205
  typer==0.12.3
206
+ # via chessli2
207
+ # via fastapi-cli
208
+ # via gradio
209
+ typing-extensions==4.11.0
210
+ # via berserk
211
+ # via fastapi
212
+ # via gradio
213
+ # via gradio-client
214
+ # via huggingface-hub
215
+ # via pydantic
216
+ # via pydantic-core
217
+ # via typer
218
+ tzdata==2024.1
219
+ # via pandas
220
+ ujson==5.9.0
221
+ # via fastapi
222
+ urllib3==2.2.1
223
+ # via gradio
224
+ # via requests
225
+ uvicorn==0.29.0
226
+ # via fastapi
227
+ # via fastapi-cli
228
+ # via gradio
229
+ uvloop==0.19.0
230
+ # via uvicorn
231
+ watchfiles==0.21.0
232
+ # via uvicorn
233
  websockets==11.0.3
234
+ # via gradio-client
235
+ # via uvicorn
236
  wrapt==1.16.0
237
+ # via deprecated
requirements.lock CHANGED
@@ -5,78 +5,222 @@
5
  # pre: false
6
  # features: []
7
  # all-features: false
 
8
 
9
  -e file:.
10
  aiofiles==23.2.1
11
- altair==5.2.0
 
 
12
  annotated-types==0.6.0
13
- anyio==4.2.0
 
 
 
 
14
  attrs==23.2.0
 
 
15
  berserk==0.13.2
16
- certifi==2023.11.17
 
 
 
 
17
  charset-normalizer==3.3.2
 
18
  chess==1.10.0
 
19
  click==8.1.7
 
 
 
 
20
  cycler==0.12.1
 
21
  deprecated==1.2.14
22
- exceptiongroup==1.2.1
23
- fastapi==0.109.0
24
- ffmpy==0.3.1
25
- filelock==3.13.1
26
- fonttools==4.47.2
27
- fsspec==2023.12.2
28
- gradio==4.29.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  gradio-calendar==0.0.4
30
- gradio-client==0.16.1
 
 
31
  h11==0.14.0
32
- httpcore==1.0.2
33
- httpx==0.26.0
34
- huggingface-hub==0.20.2
35
- idna==3.6
36
- importlib-resources==6.1.1
37
- jinja2==3.1.3
38
- jsonschema==4.20.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  jsonschema-specifications==2023.12.1
 
40
  kiwisolver==1.4.5
 
41
  markdown-it-py==3.0.0
42
- markupsafe==2.1.3
43
- matplotlib==3.5.3
 
 
 
 
44
  mdurl==0.1.2
 
45
  ndjson==0.3.1
46
- numpy==1.24.4
47
- orjson==3.9.10
48
- packaging==23.2
49
- pandas==2.0.3
50
- pillow==10.2.0
51
- pydantic==2.5.3
52
- pydantic-core==2.14.6
53
- pydantic-settings==2.1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  pydub==0.25.1
55
- pygments==2.17.2
56
- pyparsing==3.1.1
57
- python-dateutil==2.8.2
58
- python-dotenv==1.0.0
 
 
 
 
 
 
 
 
59
  python-multipart==0.0.9
60
- pytz==2023.3.post1
 
 
 
61
  pyyaml==6.0.1
62
- referencing==0.32.1
 
 
 
 
 
63
  requests==2.31.0
64
- rich==13.7.0
65
- rpds-py==0.17.1
66
- ruff==0.4.3
 
 
 
 
 
 
67
  semantic-version==2.10.0
 
68
  shellingham==1.5.4
 
69
  six==1.16.0
70
- sniffio==1.3.0
71
- starlette==0.35.1
 
 
 
 
72
  tabulate==0.9.0
 
73
  tomlkit==0.12.0
74
- toolz==0.12.0
75
- tqdm==4.66.1
 
 
 
76
  typer==0.12.3
77
- typing-extensions==4.9.0
78
- tzdata==2023.4
79
- urllib3==2.1.0
80
- uvicorn==0.25.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  websockets==11.0.3
 
 
82
  wrapt==1.16.0
 
 
5
  # pre: false
6
  # features: []
7
  # all-features: false
8
+ # with-sources: false
9
 
10
  -e file:.
11
  aiofiles==23.2.1
12
+ # via gradio
13
+ altair==5.3.0
14
+ # via gradio
15
  annotated-types==0.6.0
16
+ # via pydantic
17
+ anyio==4.3.0
18
+ # via httpx
19
+ # via starlette
20
+ # via watchfiles
21
  attrs==23.2.0
22
+ # via jsonschema
23
+ # via referencing
24
  berserk==0.13.2
25
+ # via chessli2
26
+ certifi==2024.2.2
27
+ # via httpcore
28
+ # via httpx
29
+ # via requests
30
  charset-normalizer==3.3.2
31
+ # via requests
32
  chess==1.10.0
33
+ # via chessli2
34
  click==8.1.7
35
+ # via typer
36
+ # via uvicorn
37
+ contourpy==1.2.1
38
+ # via matplotlib
39
  cycler==0.12.1
40
+ # via matplotlib
41
  deprecated==1.2.14
42
+ # via berserk
43
+ dnspython==2.6.1
44
+ # via email-validator
45
+ email-validator==2.1.1
46
+ # via fastapi
47
+ fastapi==0.111.0
48
+ # via fastapi-cli
49
+ # via gradio
50
+ fastapi-cli==0.0.3
51
+ # via fastapi
52
+ ffmpy==0.3.2
53
+ # via gradio
54
+ filelock==3.14.0
55
+ # via huggingface-hub
56
+ fonttools==4.51.0
57
+ # via matplotlib
58
+ fsspec==2024.3.1
59
+ # via gradio-client
60
+ # via huggingface-hub
61
+ gradio==4.31.0
62
+ # via chessli2
63
+ # via gradio-calendar
64
  gradio-calendar==0.0.4
65
+ # via chessli2
66
+ gradio-client==0.16.2
67
+ # via gradio
68
  h11==0.14.0
69
+ # via httpcore
70
+ # via uvicorn
71
+ httpcore==1.0.5
72
+ # via httpx
73
+ httptools==0.6.1
74
+ # via uvicorn
75
+ httpx==0.27.0
76
+ # via fastapi
77
+ # via gradio
78
+ # via gradio-client
79
+ huggingface-hub==0.23.0
80
+ # via gradio
81
+ # via gradio-client
82
+ idna==3.7
83
+ # via anyio
84
+ # via email-validator
85
+ # via httpx
86
+ # via requests
87
+ importlib-resources==6.4.0
88
+ # via gradio
89
+ jinja2==3.1.4
90
+ # via altair
91
+ # via fastapi
92
+ # via gradio
93
+ jsonschema==4.22.0
94
+ # via altair
95
  jsonschema-specifications==2023.12.1
96
+ # via jsonschema
97
  kiwisolver==1.4.5
98
+ # via matplotlib
99
  markdown-it-py==3.0.0
100
+ # via rich
101
+ markupsafe==2.1.5
102
+ # via gradio
103
+ # via jinja2
104
+ matplotlib==3.8.4
105
+ # via gradio
106
  mdurl==0.1.2
107
+ # via markdown-it-py
108
  ndjson==0.3.1
109
+ # via berserk
110
+ numpy==1.26.4
111
+ # via altair
112
+ # via contourpy
113
+ # via gradio
114
+ # via matplotlib
115
+ # via pandas
116
+ orjson==3.10.3
117
+ # via fastapi
118
+ # via gradio
119
+ packaging==24.0
120
+ # via altair
121
+ # via gradio
122
+ # via gradio-client
123
+ # via huggingface-hub
124
+ # via matplotlib
125
+ pandas==2.2.2
126
+ # via altair
127
+ # via gradio
128
+ pillow==10.3.0
129
+ # via gradio
130
+ # via matplotlib
131
+ pydantic==2.7.1
132
+ # via chessli2
133
+ # via fastapi
134
+ # via gradio
135
+ # via pydantic-settings
136
+ pydantic-core==2.18.2
137
+ # via pydantic
138
+ pydantic-settings==2.2.1
139
+ # via chessli2
140
  pydub==0.25.1
141
+ # via gradio
142
+ pygments==2.18.0
143
+ # via rich
144
+ pyparsing==3.1.2
145
+ # via matplotlib
146
+ python-dateutil==2.9.0.post0
147
+ # via berserk
148
+ # via matplotlib
149
+ # via pandas
150
+ python-dotenv==1.0.1
151
+ # via pydantic-settings
152
+ # via uvicorn
153
  python-multipart==0.0.9
154
+ # via fastapi
155
+ # via gradio
156
+ pytz==2024.1
157
+ # via pandas
158
  pyyaml==6.0.1
159
+ # via gradio
160
+ # via huggingface-hub
161
+ # via uvicorn
162
+ referencing==0.35.1
163
+ # via jsonschema
164
+ # via jsonschema-specifications
165
  requests==2.31.0
166
+ # via berserk
167
+ # via huggingface-hub
168
+ rich==13.7.1
169
+ # via typer
170
+ rpds-py==0.18.1
171
+ # via jsonschema
172
+ # via referencing
173
+ ruff==0.4.4
174
+ # via gradio
175
  semantic-version==2.10.0
176
+ # via gradio
177
  shellingham==1.5.4
178
+ # via typer
179
  six==1.16.0
180
+ # via python-dateutil
181
+ sniffio==1.3.1
182
+ # via anyio
183
+ # via httpx
184
+ starlette==0.37.2
185
+ # via fastapi
186
  tabulate==0.9.0
187
+ # via chessli2
188
  tomlkit==0.12.0
189
+ # via gradio
190
+ toolz==0.12.1
191
+ # via altair
192
+ tqdm==4.66.4
193
+ # via huggingface-hub
194
  typer==0.12.3
195
+ # via chessli2
196
+ # via fastapi-cli
197
+ # via gradio
198
+ typing-extensions==4.11.0
199
+ # via berserk
200
+ # via fastapi
201
+ # via gradio
202
+ # via gradio-client
203
+ # via huggingface-hub
204
+ # via pydantic
205
+ # via pydantic-core
206
+ # via typer
207
+ tzdata==2024.1
208
+ # via pandas
209
+ ujson==5.9.0
210
+ # via fastapi
211
+ urllib3==2.2.1
212
+ # via gradio
213
+ # via requests
214
+ uvicorn==0.29.0
215
+ # via fastapi
216
+ # via fastapi-cli
217
+ # via gradio
218
+ uvloop==0.19.0
219
+ # via uvicorn
220
+ watchfiles==0.21.0
221
+ # via uvicorn
222
  websockets==11.0.3
223
+ # via gradio-client
224
+ # via uvicorn
225
  wrapt==1.16.0
226
+ # via deprecated
requirements.txt CHANGED
@@ -1,75 +1 @@
1
- aiofiles==23.2.1
2
- altair==5.2.0
3
- annotated-types==0.6.0
4
- anyio==4.2.0
5
- attrs==23.2.0
6
- berserk==0.13.2
7
- certifi==2023.11.17
8
- charset-normalizer==3.3.2
9
- chess==1.10.0
10
- click==8.1.7
11
- cycler==0.12.1
12
- deprecated==1.2.14
13
- exceptiongroup==1.2.1
14
- fastapi==0.109.0
15
- ffmpy==0.3.1
16
- filelock==3.13.1
17
- fonttools==4.47.2
18
- fsspec==2023.12.2
19
- gradio==4.29.0
20
- gradio-calendar==0.0.4
21
- gradio-client==0.16.1
22
- h11==0.14.0
23
- httpcore==1.0.2
24
- httpx==0.26.0
25
- huggingface-hub==0.20.2
26
- idna==3.6
27
- importlib-resources==6.1.1
28
- jinja2==3.1.3
29
- jsonschema==4.20.0
30
- jsonschema-specifications==2023.12.1
31
- kiwisolver==1.4.5
32
- markdown-it-py==3.0.0
33
- markupsafe==2.1.3
34
- matplotlib==3.5.3
35
- mdurl==0.1.2
36
- ndjson==0.3.1
37
- numpy==1.24.4
38
- orjson==3.9.10
39
- packaging==23.2
40
- pandas==2.0.3
41
- pillow==10.2.0
42
- pkgutil-resolve-name==1.3.10
43
- pydantic==2.5.3
44
- pydantic-core==2.14.6
45
- pydantic-settings==2.1.0
46
- pydub==0.25.1
47
- pygments==2.17.2
48
- pyparsing==3.1.1
49
- python-dateutil==2.8.2
50
- python-dotenv==1.0.0
51
- python-multipart==0.0.9
52
- pytz==2023.3.post1
53
- pyyaml==6.0.1
54
- referencing==0.32.1
55
- requests==2.31.0
56
- rich==13.7.0
57
- rpds-py==0.17.1
58
- ruff==0.4.3
59
- semantic-version==2.10.0
60
- shellingham==1.5.4
61
- six==1.16.0
62
- sniffio==1.3.0
63
- starlette==0.35.1
64
- tabulate==0.9.0
65
- tomlkit==0.12.0
66
- toolz==0.12.0
67
- tqdm==4.66.1
68
- typer==0.12.3
69
- typing-extensions==4.9.0
70
- tzdata==2023.4
71
- urllib3==2.1.0
72
- uvicorn==0.25.0
73
- websockets==11.0.3
74
- wrapt==1.16.0
75
- zipp==3.18.1
 
1
+ chessli2 @ git+https://github.com/pwenker/chessli2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/chessli2/app.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ if __name__ == "__main__":
2
+ from chessli2.gui import chessli2_gradio_app
3
+ chessli2_gradio_app.launch()
src/chessli2/choices.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class Output(str, Enum):
5
+ file = "file"
6
+ info = "info"
7
+ pgn = "pgn"
8
+
9
+
10
+ class Color(Enum):
11
+ white = 1
12
+ black = 0
13
+
14
+
15
+ class MistakeNag(Enum):
16
+ mistake = 2
17
+ blunder = 4
18
+ speculative = 5
19
+ dubious = 6
20
+
21
+
22
+ class TimeControl(str, Enum):
23
+ bullet = "Bullet"
24
+ blitz = "Blitz"
25
+ rapid = "Rapid"
26
+ classical = "Classical"
27
+ ultraBullet = "UltraBullet"
28
+ all = "All Time Controls"
29
+
30
+
31
+ class PuzzleTheme(str, Enum):
32
+ advancedPawn = "Advanced pawn"
33
+ advantage = "Advantage"
34
+ anastasiaMate = "Anastasia's mate"
35
+ arabianMate = "Arabian mate"
36
+ attackingF2F7 = "Attacking f2 or f7"
37
+ attraction = "Attraction"
38
+ backRankMate = "Back rank mate"
39
+ bishopEndgame = "Bishop endgame"
40
+ bodenMate = "Boden's mate"
41
+ castling = "Castling"
42
+ capturingDefender = "Capture the defender"
43
+ crushing = "Crushing"
44
+ doubleBishopMate = "Double bishop mate"
45
+ dovetailMate = "Dovetail mate"
46
+ equality = "Equality"
47
+ kingsideAttack = "Kingside attack"
48
+ clearance = "Clearance"
49
+ defensiveMove = "Defensive move"
50
+ deflection = "Deflection"
51
+ discoveredAttack = "Discovered attack"
52
+ doubleCheck = "Double check"
53
+ endgame = "Endgame"
54
+ exposedKing = "Exposed king"
55
+ fork = "Fork"
56
+ hangingPiece = "Hanging piece"
57
+ hookMate = "Hook mate"
58
+ interference = "Interference"
59
+ intermezzo = "Intermezzo"
60
+ knightEndgame = "Knight endgame"
61
+ long = "Long puzzle"
62
+ master = "Master games"
63
+ masterVsMaster = "Master vs Master games"
64
+ mate = "Checkmate"
65
+ mateIn1 = "Mate in 1"
66
+ mateIn2 = "Mate in 2"
67
+ mateIn3 = "Mate in 3"
68
+ mateIn4 = "Mate in 4"
69
+ mateIn5 = "Mate in 5 or more"
70
+ middlegame = "Middlegame"
71
+ oneMove = "One-move puzzle"
72
+ opening = "Opening"
73
+ pawnEndgame = "Pawn endgame"
74
+ pin = "Pin"
75
+ promotion = "Promotion"
76
+ queenEndgame = "Queen endgame"
77
+ queenRookEndgame = "Queen and Rook"
78
+ queensideAttack = "Queenside attack"
79
+ quietMove = "Quiet move"
80
+ rookEndgame = "Rook endgame"
81
+ sacrifice = "Sacrifice"
82
+ short = "Short puzzle"
83
+ skewer = "Skewer"
84
+ smotheredMate = "Smothered mate"
85
+ superGM = "Super GM games"
86
+ trappedPiece = "Trapped piece"
87
+ underPromotion = "Underpromotion"
88
+ veryLong = "Very long puzzle"
89
+ xRayAttack = "X-Ray attack"
90
+ zugzwang = "Zugzwang"
91
+ healthyMix = "Healthy mix"
92
+ playerGames = "Player games"
src/chessli2/cli.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import typer
4
+ from gradio_client import Client
5
+ from rich import print
6
+
7
+ from chessli2.choices import MistakeNag, Output, PuzzleTheme, TimeControl
8
+ from chessli2.settings import settings
9
+
10
+ app = typer.Typer()
11
+
12
+
13
+ @app.command()
14
+ def serve(
15
+ server_name: str = typer.Option(
16
+ "127.0.0.1",
17
+ help="To make app accessible on local network, set this to '0.0.0.0'. Can be set by environment variable GRADIO_SERVER_NAME. If None, will use '127.0.0.1",
18
+ ),
19
+ server_port: int = typer.Option(
20
+ 7860,
21
+ help="Will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT. If None, will search for an available port starting at 7860.",
22
+ ),
23
+ ):
24
+ """
25
+ Starts the app and serves it given specified `server_name` and `server_port`.
26
+ """
27
+ from chessli2.gui import chessli2_gradio_app
28
+
29
+ chessli2_gradio_app.launch(server_port=server_port, server_name=server_name)
30
+
31
+
32
+ @app.command()
33
+ def mistakes(
34
+ src: str = typer.Option(
35
+ "pwenker/chessli2",
36
+ help='Select either the name of the Hugging Face Space to load, (e.g. "pwenker/chessli2") or the full URL (including "http" or "https") of the hosted Gradio app to load (e.g. "http://localhost:7860/" or "https://bec81a83-5b5c-471e.gradio.live/',
37
+ ),
38
+ lichess_api_token: str = typer.Option(
39
+ settings.lichess_api_token, help="Select lichess API token"
40
+ ),
41
+ user_name: str = typer.Option("pwenker", help="Select user name"),
42
+ start_date: str = typer.Option("2017-05-14", help="Select start date (YYYY-MM-DD)"),
43
+ end_date: str = typer.Option("2024-05-14", help="Select end date (YYYY-MM-DD)"),
44
+ nags: list[int] = typer.Option(
45
+ [MistakeNag.blunder.value, MistakeNag.mistake.value],
46
+ help="Filter by mistake nag type (blunder=4, mistake=2, speculative=5, dubious=6)",
47
+ ),
48
+ time_control: TimeControl = typer.Option(
49
+ TimeControl.all, help="Filter by time control"
50
+ ),
51
+ output: Output = typer.Option(
52
+ Output.file,
53
+ help="""Select whether to output mistakes as markdown table, as PGN string, or file path of CSV file containing PGNS""",
54
+ ),
55
+ ):
56
+ """
57
+ Fetches mistakes from the API based on the specified parameters.
58
+ """
59
+ client = Client(src)
60
+ mistake_pgns, mistake_md, mistake_csv = client.predict(
61
+ lichess_api_token=lichess_api_token,
62
+ user_name=user_name,
63
+ start_date=start_date,
64
+ end_date=end_date,
65
+ nags=nags,
66
+ time_control=time_control,
67
+ api_name="/get_mistakes",
68
+ )
69
+
70
+ if output == Output.info:
71
+ print(mistake_md)
72
+ elif output == Output.pgn:
73
+ print(mistake_pgns)
74
+ elif output == Output.file:
75
+ print(mistake_csv["value"])
76
+
77
+
78
+ @app.command()
79
+ def puzzles(
80
+ src: str = typer.Option(
81
+ "http://pwenker.github.io/chessli2/",
82
+ help="Select either the name of the Hugging Face Space to load, (e.g. 'pwenker/chessli2') or the full URL (including 'http' or 'https') of the hosted Gradio app to load (e.g. 'http://localhost:7860/' or 'https://bec81a83-5b5c-471e.gradio.live/')",
83
+ ),
84
+ lichess_api_token: str = typer.Option(
85
+ settings.lichess_api_token, help="Select lichess API token"
86
+ ),
87
+ before: str = typer.Option(
88
+ "2024-05-15",
89
+ help="Only get puzzle activity before this date (format YYYY-MM-DD)",
90
+ ),
91
+ max: int = typer.Option(100, help="Select maxmimum number of puzzles"),
92
+ themes: list[PuzzleTheme] = typer.Option(
93
+ [pt for pt in PuzzleTheme],
94
+ help="Filter by puzzle themes",
95
+ ),
96
+ output: Output = typer.Option(
97
+ Output.file,
98
+ help="Select whether to output info about mistakes, mistakes as PGN string, or a file path of CSV file containing PGNs",
99
+ ),
100
+ ):
101
+ """
102
+ Fetches puzzles from the API based on the specified parameters.
103
+ """
104
+ logging.disable(logging.CRITICAL)
105
+ client = Client(src)
106
+ puzzle_pgns, puzzle_theme_counter, puzzle_csv = client.predict(
107
+ before=before,
108
+ max=max,
109
+ themes_selection=themes,
110
+ api_name="/get_puzzles",
111
+ )
112
+
113
+ if output == Output.info:
114
+ print(puzzle_theme_counter)
115
+ elif output == Output.pgn:
116
+ print(puzzle_pgns)
117
+ elif output == Output.file:
118
+ print(puzzle_csv["value"])
119
+
120
+
121
+ if __name__ == "__main__":
122
+ app()
src/chessli2/games.py DELETED
@@ -1,68 +0,0 @@
1
- import io
2
-
3
- import berserk
4
- import chess.pgn
5
- import gradio as gr
6
- from pydantic_settings import BaseSettings, SettingsConfigDict
7
-
8
-
9
- class Settings(BaseSettings):
10
- model_config = SettingsConfigDict(env_file=".env")
11
- lichess_api_token: str = ''
12
-
13
-
14
- settings = Settings()
15
-
16
- def generate_grid_html(games_list, row_length=2):
17
- game_ids = [
18
- chess.pgn.read_headers(io.StringIO(game_pgn))["Site"].rpartition("/")[-1]
19
- for game_pgn in games_list
20
- ]
21
- if row_length == 1:
22
- return "\n".join(
23
- [
24
- f'<iframe src="https://lichess.org/embed/game/{game_id}?theme=auto&bg=auto" width=600 height=397 frameborder=0></iframe>'
25
- for game_id in game_ids
26
- ]
27
- )
28
- else:
29
- html_rows = []
30
- for i in range(0, len(game_ids), row_length):
31
- row_html = "".join(
32
- [
33
- f'<iframe src="https://lichess.org/embed/game/{id}?theme=auto&bg=auto" width="600" height="397" frameborder="0"></iframe>'
34
- for id in game_ids[i : i + row_length]
35
- ]
36
- )
37
- # Wrap each row in a div for grid formatting
38
- html_rows.append(
39
- f'<div style="display: flex; justify-content: space-around;">{row_html}</div>'
40
- )
41
- return "\n".join(html_rows)
42
-
43
- def fetch_games(
44
- user_name, start_date, end_date, lichess_api_token=settings.lichess_api_token
45
- ):
46
- gr.Info("Fetching chess games 🔄♟️")
47
-
48
- start = berserk.utils.to_millis(start_date)
49
- end = berserk.utils.to_millis(end_date)
50
- session = berserk.TokenSession(lichess_api_token)
51
- client = berserk.Client(session=session)
52
- games = client.games.export_by_player(
53
- user_name,
54
- as_pgn=True,
55
- color="black",
56
- evals=True,
57
- analysed=True,
58
- literate=True,
59
- since=start,
60
- until=end,
61
- opening=True,
62
- )
63
- games_list = list(games)
64
-
65
-
66
- game_ids_html = generate_grid_html(games_list)
67
-
68
- return games_list, gr.HTML(game_ids_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/chessli2/gui.py CHANGED
@@ -1,42 +1,44 @@
1
  from datetime import datetime
2
- from io import StringIO
3
  from pathlib import Path
4
 
5
- import chess.pgn
6
  import gradio as gr
7
- import pandas as pd
8
  from gradio_calendar import Calendar
9
 
10
- from src.chessli2.mistakes import validated_get_mistakes
 
 
11
 
12
  readme_file = Path("README.md")
 
13
 
14
- nags = {
15
- "Mistake (?)": 2,
16
- "Blunder (??)": 4,
17
- "Speculative Move (!?)": 5,
18
- "Dubious Move (?!)": 6,
19
- }
20
 
21
- with gr.Blocks() as demo:
22
  with gr.Tab("Welcome"):
23
  readme = readme_file.read_text()
24
- gr_readme = readme.split('---')[2].strip()
25
  gr.Markdown(gr_readme)
26
- with gr.Tab("Games & Mistakes"):
27
- gr.Markdown("Fetch your games and download a CSV with your mistakes")
28
 
29
- lichess_api_token = gr.Textbox(
30
- placeholder="Paste your Lichess API key",
31
- label="Lichess API Token",
32
- lines=1,
33
- type="password",
 
 
 
34
  )
 
35
  user_name = gr.Textbox(
36
  label="User Name",
37
  placeholder="Enter your user name",
38
  info="Type in your lichess user name",
39
  )
 
 
 
 
 
 
40
  with gr.Row():
41
  start_date = Calendar(
42
  type="datetime",
@@ -52,76 +54,94 @@ with gr.Blocks() as demo:
52
  )
53
  nags_checkbox = gr.CheckboxGroup(
54
  label="Mistake Types",
55
- value=[2, 4], # Mistakes and Blundes per default
56
  info="Select which types of mistakes should be detected",
57
- choices=[(k, v) for k, v in nags.items()],
 
 
 
 
 
 
 
 
 
 
 
58
  )
59
  get_mistakes_btn = gr.Button("Get Mistakes", variant="primary")
60
 
61
- gr.Markdown("### Mistakes")
62
- mistakes_df = gr.Dataframe(
63
- headers=["PGN"],
64
- visible=False,
65
- interactive=False,
66
- )
67
- mistakes_md = gr.Markdown()
68
 
69
- def export_csv(df, user_name):
70
- gr.Info("Preparing downloadable CSV file 💾")
71
- file_name = f"mistakes_{user_name}.csv"
72
- df.to_csv(
73
- file_name,
74
- index=False,
75
- header=False,
76
- )
77
- return gr.DownloadButton(value=file_name, visible=True)
78
-
79
- download_button = gr.DownloadButton(
80
  "Download CSV", variant="primary", visible=False
81
  )
82
 
83
- def df_to_md(df):
84
- gr.Info("Visualizing mistakes as markdown table 📉📝")
85
-
86
- def parse_pgn(pgn_text):
87
- pgn_io = StringIO(pgn_text)
88
- game = chess.pgn.read_game(pgn_io)
89
- headers = game.headers
90
- moves = pgn_text.split("\n\n")[1]
91
- mistake, *correct_variation = moves.split("\n")
92
- result_emoji = (
93
- "✅"
94
- if headers["Result"] == "1-0"
95
- else "❌"
96
- if headers["Result"] == "0-1"
97
- else "➖"
98
- )
99
- return {
100
- "White": headers["White"],
101
- "Black": headers["Black"],
102
- "Result": result_emoji,
103
- "White Elo": headers["WhiteElo"],
104
- "Black Elo": headers["BlackElo"],
105
- "Opening": headers["Opening"],
106
- "Mistake ❌": mistake,
107
- "Correct Variation ✅": correct_variation,
108
- }
109
-
110
- df_info = df["PGN"].apply(parse_pgn).apply(pd.Series)
111
 
112
- return df_info.to_markdown(index=False)
 
 
 
 
 
 
 
113
 
114
- get_mistakes_btn.click(
115
- fn=validated_get_mistakes,
116
- inputs=[lichess_api_token, user_name, start_date, end_date, nags_checkbox],
117
- outputs=mistakes_df,
118
- api_name="get_mistakes",
119
- ).success(
120
- fn=export_csv,
121
- inputs=[mistakes_df, user_name],
122
- outputs=download_button,
123
- ).success(
124
- fn=df_to_md,
125
- inputs=mistakes_df,
126
- outputs=mistakes_md,
 
 
 
 
 
 
127
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from datetime import datetime
 
2
  from pathlib import Path
3
 
 
4
  import gradio as gr
 
5
  from gradio_calendar import Calendar
6
 
7
+ from chessli2 import choices as ch
8
+ from chessli2.mistakes import validated_get_mistakes
9
+ from chessli2.tactics import get_puzzles
10
 
11
  readme_file = Path("README.md")
12
+ puzzle_themes_file = Path("docs/puzzle_themes.md")
13
 
 
 
 
 
 
 
14
 
15
+ with gr.Blocks() as chessli2_gradio_app:
16
  with gr.Tab("Welcome"):
17
  readme = readme_file.read_text()
18
+ gr_readme = readme.split("---")[2].strip()
19
  gr.Markdown(gr_readme)
 
 
20
 
21
+ with gr.Tab("Games & Mistakes"):
22
+ gr.Markdown(
23
+ """
24
+ ### Games & Mistakes
25
+
26
+ Here you can fetch your games and mistakes from your **game history** 🎮. You can filter by time control, type of mistakes, and date range.
27
+
28
+ When you are happy, you can download a CSV file with the selected mistakes' PGNs to practice them with Anki 📥."""
29
  )
30
+
31
  user_name = gr.Textbox(
32
  label="User Name",
33
  placeholder="Enter your user name",
34
  info="Type in your lichess user name",
35
  )
36
+ lichess_api_token_mistakes = gr.Textbox(
37
+ placeholder="Paste your Lichess API key",
38
+ label="Lichess API Token",
39
+ lines=1,
40
+ type="password",
41
+ )
42
  with gr.Row():
43
  start_date = Calendar(
44
  type="datetime",
 
54
  )
55
  nags_checkbox = gr.CheckboxGroup(
56
  label="Mistake Types",
57
+ value=[ch.MistakeNag.mistake.value, ch.MistakeNag.blunder.value],
58
  info="Select which types of mistakes should be detected",
59
+ choices=[
60
+ ("Mistake (?)", ch.MistakeNag.mistake.value),
61
+ ("Blunder (??)", ch.MistakeNag.blunder.value),
62
+ ("Speculative Move (!?)", ch.MistakeNag.speculative.value),
63
+ ("Dubious Move (?!)", ch.MistakeNag.dubious.value),
64
+ ],
65
+ )
66
+ time_control = gr.Dropdown(
67
+ label="Filter by Time Control",
68
+ value="All Time Controls",
69
+ allow_custom_value=True,
70
+ choices=[(tc.value, tc.name) for tc in ch.TimeControl],
71
  )
72
  get_mistakes_btn = gr.Button("Get Mistakes", variant="primary")
73
 
74
+ mistake_pgns = gr.Textbox(visible=False)
75
+ mistakes_md = gr.Markdown(label="Mistakes")
 
 
 
 
 
76
 
77
+ mistakes_download_button = gr.DownloadButton(
 
 
 
 
 
 
 
 
 
 
78
  "Download CSV", variant="primary", visible=False
79
  )
80
 
81
+ with gr.Tab("Tactics"):
82
+ gr.Markdown(
83
+ """
84
+ ### Tactics & Puzzles
85
+
86
+ Here you can fetch your puzzles from your [puzzle history](https://lichess.org/training/history) 🧩.
87
+
88
+ You can filter by [puzzle themes](https://lichess.org/training/themes) 🎯 and also select a maximum number of puzzles.
89
+ When you are happy, you can download a CSV file with the selected puzzles' PGNs to practice them with Anki 📥."""
90
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ lichess_api_token_tactics = gr.Textbox(
93
+ placeholder="Paste your Lichess API key",
94
+ label="Lichess API Token",
95
+ lines=1,
96
+ type="password",
97
+ )
98
+ with gr.Accordion("Info about puzzle themes 🧩", open=False):
99
+ gr.Markdown(puzzle_themes_file.read_text())
100
 
101
+ puzzle_themes = gr.Dropdown(
102
+ label="Puzzle Themes",
103
+ info="Filter puzzles by selecting themes. Per default, all available themes are selected",
104
+ multiselect=True,
105
+ value=[th.name for th in ch.PuzzleTheme],
106
+ choices=[(th.value, th.name) for th in ch.PuzzleTheme],
107
+ )
108
+ max_puzzles = gr.Slider(
109
+ label="Maximum number of puzzles",
110
+ info="Since Puzzle activity is sorted by reverse chronological order (most recent first), this selects the last n puzzles",
111
+ minimum=1,
112
+ value=100,
113
+ maximum=100000,
114
+ )
115
+ until_date = Calendar(
116
+ type="datetime",
117
+ value=datetime.now(),
118
+ label="Only get puzzle activity before this time",
119
+ info="Click the calendar icon to bring up the calendar.",
120
  )
121
+ tactics_btn = gr.Button("Get tactics", variant="primary")
122
+ puzzle_pgns = gr.Textbox(visible=False)
123
+ puzzle_info = gr.JSON(label="Number of puzzles found organized by themes")
124
+ puzzle_md_table = gr.Markdown()
125
+ puzzle_download_button = gr.DownloadButton(
126
+ "Download CSV", variant="primary", visible=False
127
+ )
128
+
129
+ # Event Handler
130
+ get_mistakes_btn.click(
131
+ fn=validated_get_mistakes,
132
+ inputs=[
133
+ lichess_api_token_mistakes,
134
+ user_name,
135
+ start_date,
136
+ end_date,
137
+ nags_checkbox,
138
+ time_control,
139
+ ],
140
+ outputs=[mistake_pgns, mistakes_md, mistakes_download_button],
141
+ api_name="get_mistakes",
142
+ )
143
+ tactics_btn.click(
144
+ fn=get_puzzles,
145
+ inputs=[until_date, max_puzzles, puzzle_themes, lichess_api_token_tactics],
146
+ outputs=[puzzle_pgns, puzzle_info, puzzle_download_button],
147
+ )
src/chessli2/main.py DELETED
@@ -1,5 +0,0 @@
1
- from src.chessli2.gui import demo
2
-
3
-
4
- if __name__ == "__main__":
5
- demo.launch(show_error=True)
 
 
 
 
 
 
src/chessli2/mistakes.py CHANGED
@@ -1,16 +1,13 @@
1
  import io
2
- from enum import Enum
3
 
 
4
  import chess
5
  import chess.pgn
6
  import gradio as gr
7
 
8
- from src.chessli2.games import fetch_games
9
-
10
-
11
- class Color(Enum):
12
- white = 1
13
- black = 0
14
 
15
 
16
  def create_mistake_pgn(game_node):
@@ -38,35 +35,80 @@ def create_mistake_pgn(game_node):
38
  return pgn_string
39
 
40
 
41
- def get_mistakes(lichess_api_token, user_name, start_date, end_date, nags):
42
- games, _ = fetch_games(user_name, start_date, end_date, lichess_api_token)
 
 
43
 
44
- gr.Info("Finding mistakes 🔍❌")
45
- mistake_pgns = []
46
- for game_pgn in games:
47
- game_pgn = io.StringIO(game_pgn)
48
- game_node = chess.pgn.read_game(game_pgn)
49
-
50
- player: Color = (
51
- Color.white if game_node.headers["White"] == user_name else Color.black
52
  )
53
 
54
- def was_players_move(game_node):
55
- return player.value != game_node.turn()
56
-
57
- def relevant_mistakes_were_made(game_node, nags):
58
- return game_node.nags and game_node.nags.issubset(set(nags))
59
-
60
- while game_node is not None:
61
- if was_players_move(game_node) and relevant_mistakes_were_made(
62
- game_node, nags
63
- ):
64
- mistake_pgn = create_mistake_pgn(game_node)
65
 
66
- mistake_pgns.append(mistake_pgn)
67
- game_node = game_node.next()
68
-
69
- return [[mp] for mp in mistake_pgns]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
 
72
  def validate_user_input(user_name, start_date, end_date):
@@ -77,8 +119,12 @@ def validate_user_input(user_name, start_date, end_date):
77
  return user_name, start_date, end_date
78
 
79
 
80
- def validated_get_mistakes(lichess_api_token, user_name, start_date, end_date, nags):
 
 
81
  user_name, start_date, end_date = validate_user_input(
82
  user_name, start_date, end_date
83
  )
84
- return get_mistakes(lichess_api_token, user_name, start_date, end_date, nags)
 
 
 
1
  import io
 
2
 
3
+ import berserk
4
  import chess
5
  import chess.pgn
6
  import gradio as gr
7
 
8
+ from chessli2.choices import Color
9
+ from chessli2.settings import get_client
10
+ from chessli2.writer import mistake_pgns_to_md, write_pgn_to_csv
 
 
 
11
 
12
 
13
  def create_mistake_pgn(game_node):
 
35
  return pgn_string
36
 
37
 
38
+ def get_mistakes(
39
+ lichess_api_token, user_name, start_date, end_date, nags, time_control
40
+ ):
41
+ client, lichess_api_token = get_client(lichess_api_token=lichess_api_token)
42
 
43
+ if not lichess_api_token:
44
+ gr.Info(
45
+ "If you authenticate with your lichess API token you can increase requests limit from 20 to 60 games per second!"
 
 
 
 
 
46
  )
47
 
48
+ games = client.games.export_by_player(
49
+ user_name,
50
+ as_pgn=True,
51
+ evals=True,
52
+ analysed=True,
53
+ literate=True,
54
+ since=berserk.utils.to_millis(start_date),
55
+ perf_type=None if time_control == "all" else time_control,
56
+ until=berserk.utils.to_millis(end_date),
57
+ opening=True,
58
+ )
59
 
60
+ mistake_pgns = []
61
+ n_games = 0
62
+ try:
63
+ for n, game_pgn in enumerate(games):
64
+ game_pgn = io.StringIO(game_pgn)
65
+ game_node = chess.pgn.read_game(game_pgn)
66
+
67
+ player: Color = (
68
+ Color.white if game_node.headers["White"] == user_name else Color.black
69
+ )
70
+
71
+ def was_players_move(game_node):
72
+ return player.value != game_node.turn()
73
+
74
+ def relevant_mistakes_were_made(game_node, nags):
75
+ return game_node.nags and game_node.nags.issubset(set(nags))
76
+
77
+ while game_node is not None:
78
+ if was_players_move(game_node) and relevant_mistakes_were_made(
79
+ game_node, nags
80
+ ):
81
+ mistake_pgn = create_mistake_pgn(game_node)
82
+
83
+ mistake_pgns.append(mistake_pgn)
84
+ game_node = game_node.next()
85
+
86
+ n_games = n
87
+ info = f"Fetched {n} games(s) with {len(mistake_pgns)} mistakes..."
88
+ yield mistake_pgns, "", gr.DownloadButton(
89
+ label=info, variant="secondary", visible=True
90
+ )
91
+
92
+ if mistake_pgns:
93
+ filename = f"{user_name}_mistakes.csv"
94
+ write_pgn_to_csv(mistake_pgns, filename=filename)
95
+ mistakes_md = mistake_pgns_to_md(mistake_pgns=mistake_pgns)
96
+ info = f"Download CSV file with PGNs of {len(mistake_pgns)} mistakes in {n_games} games"
97
+ yield mistake_pgns, mistakes_md, gr.DownloadButton(
98
+ value=filename, label=info, variant="primary"
99
+ )
100
+ else:
101
+ mistakes_md = ""
102
+ yield mistake_pgns, mistakes_md, gr.DownloadButton(
103
+ label="No mistakes found", variant="secondary"
104
+ )
105
+ except berserk.exceptions.ResponseError:
106
+ gr.Warning("Your lichess API token is invalid. Did you misspell it?")
107
+ yield "", "", gr.DownloadButton(
108
+ label="Please insert a correct lichess API token into the textbox at the top of the interface or leave it empty",
109
+ visible=True,
110
+ variant="stop",
111
+ )
112
 
113
 
114
  def validate_user_input(user_name, start_date, end_date):
 
119
  return user_name, start_date, end_date
120
 
121
 
122
+ def validated_get_mistakes(
123
+ lichess_api_token, user_name, start_date, end_date, nags, time_control
124
+ ):
125
  user_name, start_date, end_date = validate_user_input(
126
  user_name, start_date, end_date
127
  )
128
+ yield from get_mistakes(
129
+ lichess_api_token, user_name, start_date, end_date, nags, time_control
130
+ )
src/chessli2/rich_logging.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This file contains the logging configuration for the project.
3
+ """
4
+
5
+ import logging
6
+
7
+ from rich.console import Console
8
+ from rich.logging import RichHandler
9
+
10
+ FORMAT = "%(message)s"
11
+ logging.basicConfig(
12
+ level="INFO",
13
+ format=FORMAT,
14
+ datefmt="[%X]",
15
+ handlers=[RichHandler(rich_tracebacks=True)],
16
+ )
17
+
18
+ console = Console()
19
+ log = logging.getLogger("rich")
20
+
21
+
22
+ def set_log_level(verbosity: int):
23
+ if verbosity == 0:
24
+ log.setLevel(logging.CRITICAL)
25
+ elif verbosity == 1:
26
+ log.setLevel(logging.ERROR)
27
+ if verbosity == 2:
28
+ log.setLevel(logging.WARNING)
29
+ elif verbosity == 3:
30
+ log.setLevel(logging.INFO)
31
+ elif verbosity == 4:
32
+ log.setLevel(logging.DEBUG)
33
+ return log
src/chessli2/settings.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import berserk
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ model_config = SettingsConfigDict(env_file=".env")
7
+ lichess_api_token: str = ""
8
+
9
+
10
+ settings = Settings()
11
+
12
+
13
+ def get_client(lichess_api_token):
14
+ if not lichess_api_token:
15
+ lichess_api_token = settings.lichess_api_token
16
+ session = berserk.TokenSession(lichess_api_token)
17
+ client = berserk.Client(session=session)
18
+ return client, lichess_api_token
src/chessli2/tactics.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ from collections import Counter
3
+
4
+ import berserk
5
+ import chess
6
+ import gradio as gr
7
+
8
+ from chessli2.settings import get_client
9
+ from chessli2.writer import write_pgn_to_csv
10
+
11
+
12
+ def puzzle_pgn(puzzle_activity):
13
+ game = puzzle_activity["game"]
14
+ puzzle = puzzle_activity["puzzle"]
15
+ themes = puzzle["themes"]
16
+
17
+ board = chess.Board()
18
+ moves = game["pgn"].split()
19
+
20
+ for move in moves[:-1]:
21
+ board.push_san(move)
22
+ last_move = board.parse_san(moves[-1])
23
+
24
+ pgn = chess.pgn.Game(
25
+ headers={
26
+ "Event": "Puzzle",
27
+ "Site": f"https://www.lichess.org/training/{puzzle['id']}",
28
+ "Themes": " ".join(themes),
29
+ }
30
+ )
31
+ pgn.setup(board)
32
+ puzzle_moves = [chess.Move.from_uci(m) for m in puzzle["solution"]]
33
+
34
+ pgn.add_line(
35
+ moves=[last_move] + puzzle_moves,
36
+ starting_comment=f"Puzzle {puzzle['id']} with themes: {' '.join(puzzle['themes'])}",
37
+ )
38
+
39
+ exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
40
+ pgn_string = pgn.accept(exporter)
41
+ return pgn_string, themes
42
+
43
+
44
+ def get_puzzles(before, max, themes_selection, lichess_api_token):
45
+ client, lichess_api_token = get_client(lichess_api_token=lichess_api_token)
46
+
47
+ puzzle_activity = client.puzzles.get_puzzle_activity(
48
+ before=berserk.utils.to_millis(before), max=max
49
+ )
50
+ n_puzzles = 0
51
+ skipped_puzzles = 0
52
+ puzzle_pgns = []
53
+ # Create a counter object that tracks the number of puzzles per theme
54
+ theme_counter = Counter()
55
+
56
+ try:
57
+ for pa in puzzle_activity:
58
+ ppgn, themes = puzzle_pgn(client.puzzles.get(pa["puzzle"]["id"]))
59
+ # check if any of the selected themes are in the puzzle themes
60
+ if not set(themes_selection).isdisjoint(themes):
61
+ n_puzzles += 1
62
+ theme_counter.update(themes)
63
+ puzzle_pgns.append(ppgn)
64
+ else:
65
+ skipped_puzzles += 1
66
+ download_info = (
67
+ f"Fetched {n_puzzles}, skipped {skipped_puzzles} puzzle(s)..."
68
+ )
69
+ yield puzzle_pgns, theme_counter, gr.DownloadButton(
70
+ label=download_info, variant="secondary", visible=True
71
+ )
72
+
73
+ if n_puzzles > 0:
74
+ filename = "puzzles.csv"
75
+ write_pgn_to_csv(puzzle_pgns, filename=filename)
76
+ download_info = f"Download CSV file with PGNs of {n_puzzles} puzzles"
77
+ yield puzzle_pgns, theme_counter, gr.DownloadButton(
78
+ value=filename, label=download_info, variant="primary"
79
+ )
80
+ else:
81
+ yield puzzle_pgns, {"Puzzles": 0}, gr.DownloadButton(
82
+ label="No puzzles found", variant="secondary"
83
+ )
84
+ except berserk.exceptions.ResponseError as e:
85
+ if "No such token" in e.message:
86
+ gr.Warning("Your lichess API token is invalid. Did you misspell it?")
87
+ warning = "Please insert a correct lichess API token into the textbox at the top of the interface"
88
+ else:
89
+ gr.Warning(
90
+ "To fetch your puzzle activity, you need to authenticate with your lichess API token!"
91
+ )
92
+ warning = "Please paste your lichess API token into the corresponding textbox at the top of the interface to fetch your puzzles"
93
+ yield "", {}, gr.DownloadButton(
94
+ label=warning,
95
+ visible=True,
96
+ variant="stop",
97
+ )
98
+
99
+
100
+ def create_markdown_table(entries):
101
+ if not entries:
102
+ return "No data provided"
103
+
104
+ if not isinstance(entries, list) or not all(
105
+ isinstance(item, dict) for item in entries
106
+ ):
107
+ return "Invalid input format. Please provide a list of dictionaries."
108
+
109
+ # Extract headers from the first dictionary
110
+ headers = entries[0].keys()
111
+
112
+ # Create the header row
113
+ header_row = "| " + " | ".join(headers) + " |"
114
+
115
+ # Create the separator row
116
+ separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
117
+
118
+ # Initialize the table with the header and separator
119
+ table = [header_row, separator_row]
120
+
121
+ # Fill in the data rows
122
+ for entry in entries:
123
+ row = []
124
+ for header in headers:
125
+ if header in entry:
126
+ value = entry[header]
127
+ if isinstance(value, datetime.datetime):
128
+ value = value.strftime("%Y-%m-%d %H:%M:%S %Z")
129
+ elif isinstance(value, list):
130
+ value = ", ".join(map(str, value))
131
+ row.append(str(value))
132
+ else:
133
+ row.append("N/A")
134
+ table.append("| " + " | ".join(row) + " |")
135
+
136
+ return "\n".join(table)
src/chessli2/writer.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ from io import StringIO
3
+
4
+ import chess
5
+ import gradio as gr
6
+ import pandas as pd
7
+
8
+
9
+ def write_pgn_to_csv(pgns, filename="pgns.csv"):
10
+ """
11
+ Writes a list of PGN strings to a CSV file with a single header 'PGN'.
12
+
13
+ Args:
14
+ pgns (list of str): List containing PGN strings.
15
+ filename (str): Name of the CSV file to be created.
16
+ """
17
+ # Open the file in write mode
18
+ with open(filename, mode="w", newline="", encoding="utf-8") as file:
19
+ # Create a CSV writer object
20
+ writer = csv.writer(file)
21
+
22
+ # Write each PGN as a new row
23
+ for pgn in pgns:
24
+ writer.writerow([pgn])
25
+
26
+
27
+ def mistake_pgns_to_md(mistake_pgns):
28
+ gr.Info("Visualizing mistakes as markdown table 📉📝")
29
+
30
+ def parse_pgn(pgn_text):
31
+ pgn_io = StringIO(pgn_text)
32
+ game = chess.pgn.read_game(pgn_io)
33
+ headers = game.headers
34
+ moves = pgn_text.split("\n\n")[1]
35
+ mistake, *correct_variation = moves.split("\n")
36
+ result_emoji = (
37
+ "✅"
38
+ if headers["Result"] == "1-0"
39
+ else "❌"
40
+ if headers["Result"] == "0-1"
41
+ else "➖"
42
+ )
43
+ return {
44
+ "White": headers["White"],
45
+ "Black": headers["Black"],
46
+ "Result": result_emoji,
47
+ "White Elo": headers["WhiteElo"],
48
+ "Black Elo": headers["BlackElo"],
49
+ "Opening": headers["Opening"],
50
+ "Time Control": headers["TimeControl"],
51
+ "Mistake ❌": mistake,
52
+ "Correct Variation ✅": correct_variation,
53
+ }
54
+
55
+ df = pd.DataFrame(data=mistake_pgns, columns=["PGN"])
56
+ df_info = df["PGN"].apply(parse_pgn).apply(pd.Series)
57
+
58
+ return df_info.to_markdown(index=False)
tests/test_cli.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from typer.testing import CliRunner
3
+
4
+ from chessli2.cli import app
5
+ from chessli2.settings import settings
6
+
7
+ runner = CliRunner()
8
+
9
+ sources = ["pwenker/chessli2", "http://localhost:7860"]
10
+
11
+
12
+ @pytest.mark.parametrize("src", ["http://localhost:7860"])
13
+ def test_mistakes_with_options(src):
14
+ result = runner.invoke(
15
+ app,
16
+ [
17
+ "mistakes",
18
+ "--src",
19
+ src,
20
+ "--lichess-api-token",
21
+ settings.lichess_api_token,
22
+ "--user-name",
23
+ "pwenker",
24
+ "--start-date",
25
+ "2017-05-14",
26
+ "--end-date",
27
+ "2024-05-14",
28
+ "--nags",
29
+ "4",
30
+ "--nags",
31
+ "2",
32
+ "--time-control",
33
+ "All Time Controls",
34
+ "--output",
35
+ "file",
36
+ ],
37
+ )
38
+ assert result.exit_code == 0
39
+
40
+
41
+ @pytest.mark.parametrize("src", ["http://localhost:7860"])
42
+ def test_puzzles_with_options(src):
43
+ """
44
+ chessli puzzles --src http://localhost:7860 --user-name pwenker --before 2024-05-14 --max 100 --output file
45
+ """
46
+ result = runner.invoke(
47
+ app,
48
+ [
49
+ "puzzles",
50
+ "--src",
51
+ src,
52
+ "--lichess-api-token",
53
+ settings.lichess_api_token,
54
+ "--before",
55
+ "2024-05-14",
56
+ "--max",
57
+ '10',
58
+ "--output",
59
+ "file",
60
+ ],
61
+ )
62
+ __import__('pdb').set_trace()
63
+ assert result.exit_code == 0