libscid 0.10.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
libscid/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """Python bindings for libscid."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from ._cursor import Cursor
6
+ from ._database import Database
7
+ from ._database_filters import DatabaseFilters
8
+ from ._database_search import DatabaseSearch, HeaderCriteria
9
+ from ._domain_support._movetext_iteration import (
10
+ MovetextEvent,
11
+ MovetextLineEnd,
12
+ MovetextLineStart,
13
+ MovetextMove,
14
+ )
15
+ from ._filter import Filter
16
+ from ._game import Game
17
+ from ._move_metadata import MoveMetadata
18
+ from ._nag import Nag
19
+ from ._native import LibScidError
20
+ from ._pgn import PgnOptions
21
+ from ._position import Position
22
+
23
+ try:
24
+ __version__ = version("libscid")
25
+ except PackageNotFoundError:
26
+ __version__ = "0.0.0"
27
+
28
+ __all__ = [
29
+ "Cursor",
30
+ "Database",
31
+ "DatabaseFilters",
32
+ "DatabaseSearch",
33
+ "Filter",
34
+ "Game",
35
+ "HeaderCriteria",
36
+ "LibScidError",
37
+ "MoveMetadata",
38
+ "MovetextEvent",
39
+ "MovetextLineEnd",
40
+ "MovetextLineStart",
41
+ "MovetextMove",
42
+ "Nag",
43
+ "PgnOptions",
44
+ "Position",
45
+ "__version__",
46
+ ]
libscid/_arbiter.py ADDED
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ._cursor import Cursor
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class _PositionId:
12
+ board: str
13
+ side_to_move: str
14
+ castling: str
15
+ en_passant: str
16
+
17
+ @classmethod
18
+ def from_fen(cls, fen: str) -> _PositionId:
19
+ board, side_to_move, castling, en_passant, *_ = fen.split()
20
+ return cls(board, side_to_move, castling, en_passant)
21
+
22
+
23
+ class Arbiter:
24
+ def __init__(self, cursor: Cursor):
25
+ self._cursor = cursor
26
+
27
+ @property
28
+ def can_claim_fifty_move_rule(self) -> bool:
29
+ return self._cursor.position.halfmove_clock >= 100
30
+
31
+ @property
32
+ def can_claim_threefold_repetition(self) -> bool:
33
+ position_ids = self._path_position_ids()
34
+ return position_ids.count(position_ids[-1]) >= 3
35
+
36
+ def _path_position_ids(self) -> list[_PositionId]:
37
+ position = self._cursor._game.start_position
38
+ position_ids = [_PositionId.from_fen(position.fen)]
39
+ for move in self._cursor._path_move_uci():
40
+ position.apply_uci(move)
41
+ position_ids.append(_PositionId.from_fen(position.fen))
42
+ return position_ids
libscid/_cursor.py ADDED
@@ -0,0 +1,290 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ from collections.abc import Iterator
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from ._arbiter import Arbiter
8
+ from ._nag import Nag
9
+ from ._native import NativeLibrary
10
+ from ._position import Position
11
+
12
+ if TYPE_CHECKING:
13
+ from ._domain_support._movetext_iteration import MovetextEvent
14
+
15
+
16
+ class Cursor:
17
+ _native: NativeLibrary
18
+ _game: Any
19
+ _handle: ctypes.c_void_p
20
+
21
+ def __init__(self):
22
+ raise TypeError("Cursor objects are returned by libscid APIs")
23
+
24
+ @classmethod
25
+ def _from_handle(
26
+ cls,
27
+ native: NativeLibrary,
28
+ game: Any,
29
+ handle: ctypes.c_void_p,
30
+ ) -> Cursor:
31
+ cursor = cls.__new__(cls)
32
+ cursor._native = native
33
+ cursor._game = game
34
+ cursor._handle = handle
35
+ return cursor
36
+
37
+ def clone(self) -> Cursor:
38
+ return self._from_handle(
39
+ self._native,
40
+ self._game,
41
+ self._native.game_clone_cursor(self._game._handle, self._handle),
42
+ )
43
+
44
+ def next(self) -> Cursor | None:
45
+ return self._from_optional_handle(self._native.cursor_next(self._handle))
46
+
47
+ def previous(self) -> Cursor | None:
48
+ return self._from_optional_handle(self._native.cursor_previous(self._handle))
49
+
50
+ def to_game_start(self) -> Cursor:
51
+ return self._from_handle(
52
+ self._native,
53
+ self._game,
54
+ self._native.cursor_to_game_start(self._handle),
55
+ )
56
+
57
+ def to_game_end(self) -> Cursor:
58
+ return self._from_handle(
59
+ self._native,
60
+ self._game,
61
+ self._native.cursor_to_game_end(self._handle),
62
+ )
63
+
64
+ def to_main_line_offset(self, offset: int) -> Cursor | None:
65
+ return self._from_optional_handle(
66
+ self._native.cursor_to_main_line_offset(self._handle, offset)
67
+ )
68
+
69
+ def enter_variation(self, index: int) -> Cursor | None:
70
+ return self._from_optional_handle(
71
+ self._native.cursor_enter_variation(self._handle, index)
72
+ )
73
+
74
+ def exit_variation(self) -> Cursor | None:
75
+ return self._from_optional_handle(
76
+ self._native.cursor_exit_variation(self._handle)
77
+ )
78
+
79
+ def append_move(self, san: str | bytes) -> Cursor:
80
+ self._require_line_end("append_move")
81
+ return self._from_handle(
82
+ self._native,
83
+ self._game,
84
+ self._native.cursor_append_move(self._game._handle, self._handle, san),
85
+ )
86
+
87
+ def append_game(self, source_game: Any) -> Cursor:
88
+ self._require_line_end("append_game")
89
+ return self._from_handle(
90
+ self._native,
91
+ self._game,
92
+ self._native.cursor_append_game(
93
+ self._game._handle, self._handle, source_game._handle
94
+ ),
95
+ )
96
+
97
+ def add_variation(self, preceding_comment: str | bytes = "") -> Cursor | None:
98
+ return self._from_optional_handle(
99
+ self._native.cursor_add_variation(
100
+ self._game._handle, self._handle, preceding_comment
101
+ )
102
+ )
103
+
104
+ def remove_variation(self) -> Cursor | None:
105
+ return self._from_optional_handle(
106
+ self._native.cursor_remove_variation(self._game._handle, self._handle)
107
+ )
108
+
109
+ def promote_variation_to_first(self) -> Cursor | None:
110
+ return self._from_optional_handle(
111
+ self._native.cursor_promote_variation_to_first(
112
+ self._game._handle, self._handle
113
+ )
114
+ )
115
+
116
+ def promote_variation_to_mainline(self) -> Cursor | None:
117
+ return self._from_optional_handle(
118
+ self._native.cursor_promote_variation_to_mainline(
119
+ self._game._handle, self._handle
120
+ )
121
+ )
122
+
123
+ def truncate(self) -> Cursor:
124
+ return self._from_handle(
125
+ self._native,
126
+ self._game,
127
+ self._native.cursor_truncate(self._game._handle, self._handle),
128
+ )
129
+
130
+ def truncate_before(self) -> Cursor:
131
+ return self._from_handle(
132
+ self._native,
133
+ self._game,
134
+ self._native.cursor_truncate_before(self._game._handle, self._handle),
135
+ )
136
+
137
+ def iter_movetext(self, *, variations: bool = True) -> Iterator[MovetextEvent]:
138
+ from ._domain_support._movetext_iteration import iter_movetext
139
+
140
+ return iter_movetext(self, variations=variations)
141
+
142
+ def _from_optional_handle(self, handle: ctypes.c_void_p | None) -> Cursor | None:
143
+ if handle is None:
144
+ return None
145
+ return self._from_handle(self._native, self._game, handle)
146
+
147
+ def _require_line_end(self, method: str) -> None:
148
+ if not self.is_line_end:
149
+ raise ValueError(f"{method} requires cursor at line end")
150
+
151
+ def _path_move_uci(self) -> tuple[str, ...]:
152
+ cursor = self.clone()
153
+ moves = []
154
+ while True:
155
+ move = cursor.previous_move_uci
156
+ if move is not None:
157
+ moves.append(move)
158
+ previous = cursor.previous()
159
+ if previous is None:
160
+ raise RuntimeError("Cursor previous move disappeared")
161
+ cursor = previous
162
+ continue
163
+
164
+ parent = cursor.exit_variation()
165
+ if parent is not None:
166
+ cursor = parent
167
+ continue
168
+
169
+ return tuple(reversed(moves))
170
+
171
+ @property
172
+ def arbiter(self) -> Arbiter:
173
+ return Arbiter(self)
174
+
175
+ @property
176
+ def previous_move_san(self) -> str | None:
177
+ if self.is_line_start:
178
+ return None
179
+ return self._native.cursor_previous_move_san(self._handle)
180
+
181
+ @property
182
+ def next_move_san(self) -> str | None:
183
+ if self.is_line_end:
184
+ return None
185
+ return self._native.cursor_next_move_san(self._handle)
186
+
187
+ @property
188
+ def previous_move_uci(self) -> str | None:
189
+ if self.is_line_start:
190
+ return None
191
+ return self._native.cursor_previous_move_uci(self._handle)
192
+
193
+ @property
194
+ def next_move_uci(self) -> str | None:
195
+ if self.is_line_end:
196
+ return None
197
+ return self._native.cursor_next_move_uci(self._handle)
198
+
199
+ @property
200
+ def previous_move_nags(self) -> tuple[Nag, ...] | None:
201
+ if self.is_line_start:
202
+ return None
203
+ return tuple(
204
+ Nag(code) for code in self._native.cursor_previous_move_nags(self._handle)
205
+ )
206
+
207
+ @property
208
+ def next_move_nags(self) -> tuple[Nag, ...] | None:
209
+ if self.is_line_end:
210
+ return None
211
+ return tuple(
212
+ Nag(code) for code in self._native.cursor_next_move_nags(self._handle)
213
+ )
214
+
215
+ @property
216
+ def comment(self) -> str | None:
217
+ if self.is_line_start:
218
+ return None
219
+ return self._native.cursor_comment(self._handle)
220
+
221
+ @property
222
+ def preceding_comment(self) -> str | None:
223
+ if not self.is_line_start:
224
+ return None
225
+ return self._native.cursor_comment(self._handle)
226
+
227
+ def set_comment(self, comment: str | bytes) -> None:
228
+ self._native.cursor_set_comment(self._game._handle, self._handle, comment)
229
+
230
+ def remove_comment(self) -> None:
231
+ self.set_comment("")
232
+
233
+ def add_nag(self, nag: Nag) -> bool:
234
+ return self._native.cursor_add_nag(self._game._handle, self._handle, nag.code)
235
+
236
+ def remove_move_nag(self) -> bool:
237
+ return self._native.cursor_remove_nag(
238
+ self._game._handle, self._handle, move_nag=True
239
+ )
240
+
241
+ def remove_position_nag(self) -> bool:
242
+ return self._native.cursor_remove_nag(
243
+ self._game._handle, self._handle, move_nag=False
244
+ )
245
+
246
+ def remove_nags(self) -> None:
247
+ self._native.cursor_remove_nags(self._game._handle, self._handle)
248
+
249
+ @property
250
+ def variation_count(self) -> int:
251
+ return self._native.cursor_variation_count(self._handle)
252
+
253
+ @property
254
+ def variation_depth(self) -> int:
255
+ return self._native.cursor_variation_depth(self._handle)
256
+
257
+ @property
258
+ def variation_index(self) -> int:
259
+ return self._native.cursor_variation_index(self._handle)
260
+
261
+ @property
262
+ def is_main_line(self) -> bool:
263
+ return self.variation_depth == 0
264
+
265
+ @property
266
+ def is_variation_line(self) -> bool:
267
+ return self.variation_depth > 0
268
+
269
+ @property
270
+ def is_line_start(self) -> bool:
271
+ return self._native.cursor_is_line_start(self._handle)
272
+
273
+ @property
274
+ def is_line_end(self) -> bool:
275
+ return self._native.cursor_is_line_end(self._handle)
276
+
277
+ @property
278
+ def position(self) -> Position:
279
+ return Position._from_handle(
280
+ self._native, self._native.cursor_position(self._handle)
281
+ )
282
+
283
+ def _dispose(self) -> None:
284
+ handle = getattr(self, "_handle", None)
285
+ if handle:
286
+ self._native.free_cursor(self._handle)
287
+ self._handle = ctypes.c_void_p()
288
+
289
+ def __del__(self) -> None:
290
+ self._dispose()
libscid/_database.py ADDED
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import os
5
+ from collections.abc import Callable
6
+
7
+ from ._database_filters import DatabaseFilters
8
+ from ._database_search import DatabaseSearch
9
+ from ._game import Game
10
+ from ._native import NativeLibrary, load_library
11
+
12
+ ProgressReportCallback = Callable[[int, int, str | None], None]
13
+ ShouldCancelFn = Callable[[], bool]
14
+
15
+
16
+ class Database:
17
+ _native: NativeLibrary
18
+ _handle: ctypes.c_void_p
19
+ _filters: DatabaseFilters
20
+ _search: DatabaseSearch
21
+
22
+ def __init__(self):
23
+ raise TypeError("Database objects are returned by libscid APIs")
24
+
25
+ @classmethod
26
+ def open_pgn_read_only(
27
+ cls,
28
+ path: str | os.PathLike[str],
29
+ progress_report_callback: ProgressReportCallback | None = None,
30
+ should_cancel: ShouldCancelFn | None = None,
31
+ ) -> Database:
32
+ native = load_library()
33
+ return cls._from_handle(
34
+ native,
35
+ native.open_pgn_database_read_only(
36
+ path,
37
+ progress_report_callback=progress_report_callback,
38
+ should_cancel=should_cancel,
39
+ ),
40
+ )
41
+
42
+ @classmethod
43
+ def _from_handle(cls, native: NativeLibrary, handle: ctypes.c_void_p) -> Database:
44
+ database = cls.__new__(cls)
45
+ database._native = native
46
+ database._handle = handle
47
+ database._filters = DatabaseFilters._from_database(native, database)
48
+ database._search = DatabaseSearch._from_database(native, database)
49
+ return database
50
+
51
+ @property
52
+ def type(self) -> str:
53
+ return self._native.database_type(self._handle)
54
+
55
+ @property
56
+ def read_only(self) -> bool:
57
+ return self._native.database_read_only(self._handle)
58
+
59
+ @property
60
+ def game_count(self) -> int:
61
+ return self._native.database_game_count(self._handle)
62
+
63
+ @property
64
+ def filters(self) -> DatabaseFilters:
65
+ return self._filters
66
+
67
+ @property
68
+ def search(self) -> DatabaseSearch:
69
+ return self._search
70
+
71
+ def get_tag(self, index: int, name: str | bytes) -> str:
72
+ return self._native.database_game_tag(self._handle, index, name)
73
+
74
+ def get_game(self, index: int) -> Game:
75
+ return Game._from_handle(
76
+ self._native, self._native.database_game(self._handle, index)
77
+ )
78
+
79
+ def close(self) -> None:
80
+ handle = getattr(self, "_handle", None)
81
+ if handle:
82
+ self._native.close_database(handle)
83
+
84
+ def _dispose(self) -> None:
85
+ handle = getattr(self, "_handle", None)
86
+ if handle:
87
+ self._native.free_database(handle)
88
+ self._handle = ctypes.c_void_p()
89
+
90
+ def __del__(self) -> None:
91
+ self._dispose()
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ._filter import Filter
6
+ from ._native import NativeLibrary
7
+ from ._native._constants import SCID_FILTER_ALL_GAMES, SCID_FILTER_PRIMARY
8
+
9
+ if TYPE_CHECKING:
10
+ from ._database import Database
11
+
12
+
13
+ class DatabaseFilters:
14
+ _native: NativeLibrary
15
+ _database: Database
16
+
17
+ def __init__(self):
18
+ raise TypeError("DatabaseFilters objects are returned by libscid APIs")
19
+
20
+ @classmethod
21
+ def _from_database(
22
+ cls, native: NativeLibrary, database: Database
23
+ ) -> DatabaseFilters:
24
+ filters = cls.__new__(cls)
25
+ filters._native = native
26
+ filters._database = database
27
+ return filters
28
+
29
+ @property
30
+ def all_games(self) -> Filter:
31
+ return Filter._from_id(
32
+ self._native, self._database, SCID_FILTER_ALL_GAMES, owned=False
33
+ )
34
+
35
+ @property
36
+ def primary(self) -> Filter:
37
+ return Filter._from_id(
38
+ self._native, self._database, SCID_FILTER_PRIMARY, owned=False
39
+ )
40
+
41
+ def create(self) -> Filter:
42
+ return Filter._from_id(
43
+ self._native,
44
+ self._database,
45
+ self._native.database_filter_create(self._database._handle),
46
+ owned=True,
47
+ )