rust-pgn-reader-python-binding 3.4.0__tar.gz → 4.0.0__tar.gz

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.
Files changed (30) hide show
  1. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/Cargo.lock +16 -5
  2. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/Cargo.toml +7 -6
  3. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/PKG-INFO +1 -1
  4. rust_pgn_reader_python_binding-4.0.0/README.md +64 -0
  5. rust_pgn_reader_python_binding-4.0.0/benches/parquet_bench.rs +154 -0
  6. rust_pgn_reader_python_binding-4.0.0/rust_pgn_reader_python_binding.pyi +384 -0
  7. rust_pgn_reader_python_binding-4.0.0/src/bench_data_access.py +95 -0
  8. rust_pgn_reader_python_binding-3.4.0/src/bench_parquet.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_game.py +3 -5
  9. rust_pgn_reader_python_binding-3.4.0/src/bench.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_game_pgn.py +6 -7
  10. rust_pgn_reader_python_binding-3.4.0/src/bench_parquet_arrow.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_games.py +2 -6
  11. rust_pgn_reader_python_binding-3.4.0/src/bench_parquet_parallel.py → rust_pgn_reader_python_binding-4.0.0/src/bench_parse_games_from_strings.py +1 -3
  12. rust_pgn_reader_python_binding-4.0.0/src/board_serialization.rs +133 -0
  13. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/example_chess960.py +7 -11
  14. rust_pgn_reader_python_binding-4.0.0/src/example_complex.py +41 -0
  15. rust_pgn_reader_python_binding-4.0.0/src/example_parse_games.py +169 -0
  16. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/example_rowgroup.py +4 -3
  17. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/example_str.py +13 -12
  18. rust_pgn_reader_python_binding-4.0.0/src/lib.rs +349 -0
  19. rust_pgn_reader_python_binding-4.0.0/src/python_bindings.rs +877 -0
  20. rust_pgn_reader_python_binding-4.0.0/src/test.py +949 -0
  21. rust_pgn_reader_python_binding-4.0.0/src/visitor.rs +849 -0
  22. rust_pgn_reader_python_binding-3.4.0/README.md +0 -45
  23. rust_pgn_reader_python_binding-3.4.0/benches/parquet_bench.rs +0 -63
  24. rust_pgn_reader_python_binding-3.4.0/rust_pgn_reader_python_binding.pyi +0 -54
  25. rust_pgn_reader_python_binding-3.4.0/src/lib.rs +0 -776
  26. rust_pgn_reader_python_binding-3.4.0/src/test.py +0 -684
  27. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/.github/workflows/CI.yml +0 -0
  28. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/.gitignore +0 -0
  29. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/pyproject.toml +0 -0
  30. {rust_pgn_reader_python_binding-3.4.0 → rust_pgn_reader_python_binding-4.0.0}/src/comment_parsing.rs +0 -0
@@ -749,6 +749,15 @@ version = "3.0.4"
749
749
  source = "registry+https://github.com/rust-lang/crates.io-index"
750
750
  checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
751
751
 
752
+ [[package]]
753
+ name = "inventory"
754
+ version = "0.3.21"
755
+ source = "registry+https://github.com/rust-lang/crates.io-index"
756
+ checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
757
+ dependencies = [
758
+ "rustversion",
759
+ ]
760
+
752
761
  [[package]]
753
762
  name = "itertools"
754
763
  version = "0.13.0"
@@ -1080,9 +1089,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
1080
1089
 
1081
1090
  [[package]]
1082
1091
  name = "pgn-reader"
1083
- version = "0.28.0"
1092
+ version = "0.29.0"
1084
1093
  source = "registry+https://github.com/rust-lang/crates.io-index"
1085
- checksum = "d0e07db84c19e093741643903738788e6f773237c7eeb60f9a40191afc2a815a"
1094
+ checksum = "b822a2cfc1c90c109ea35458ad9bff6669198fdd4ea8de1132e702b4d9f64b88"
1086
1095
  dependencies = [
1087
1096
  "btoi",
1088
1097
  "memchr",
@@ -1175,6 +1184,7 @@ dependencies = [
1175
1184
  "chrono-tz",
1176
1185
  "indexmap",
1177
1186
  "indoc",
1187
+ "inventory",
1178
1188
  "libc",
1179
1189
  "memoffset",
1180
1190
  "once_cell",
@@ -1322,13 +1332,14 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
1322
1332
 
1323
1333
  [[package]]
1324
1334
  name = "rust_pgn_reader_python_binding"
1325
- version = "3.4.0"
1335
+ version = "4.0.0"
1326
1336
  dependencies = [
1327
1337
  "arrow",
1328
1338
  "arrow-array",
1329
1339
  "criterion",
1330
1340
  "nom",
1331
1341
  "num_cpus",
1342
+ "numpy",
1332
1343
  "parquet",
1333
1344
  "pgn-reader",
1334
1345
  "pyo3",
@@ -1430,9 +1441,9 @@ dependencies = [
1430
1441
 
1431
1442
  [[package]]
1432
1443
  name = "shakmaty"
1433
- version = "0.29.4"
1444
+ version = "0.30.0"
1434
1445
  source = "registry+https://github.com/rust-lang/crates.io-index"
1435
- checksum = "7d0941d24adb219ae06d990af92633dea7486831ca00d6c3988c371e4e575035"
1446
+ checksum = "41cef923c4ad8bf68e6f52a3091499108b8fec71ea8c4ba27d1acfb263f53241"
1436
1447
  dependencies = [
1437
1448
  "arrayvec",
1438
1449
  "bitflags",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rust_pgn_reader_python_binding"
3
- version = "3.4.0"
3
+ version = "4.0.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -11,19 +11,20 @@ crate-type = ["lib", "cdylib"]
11
11
  # lib is needed for the native benchmark
12
12
 
13
13
  [dependencies]
14
- pyo3 = "0.27.2"
15
- shakmaty = "0.29"
16
- pgn-reader = "0.28.0"
14
+ pyo3 = { version = "0.27.2", features = ["multiple-pymethods"] }
15
+ shakmaty = "0.30"
16
+ pgn-reader = "0.29.0"
17
17
  nom = "8.0"
18
18
  rayon = "1.11"
19
19
  num_cpus = "1.17"
20
20
  arrow-array = "57"
21
21
  pyo3-arrow = "0.15"
22
+ numpy = "0.27"
23
+ parquet = "57"
24
+ arrow = "57"
22
25
 
23
26
  [dev-dependencies]
24
27
  criterion = "0.8"
25
- parquet = "57"
26
- arrow = "57"
27
28
 
28
29
  [[bench]]
29
30
  harness = false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rust_pgn_reader_python_binding
3
- Version: 3.4.0
3
+ Version: 4.0.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -0,0 +1,64 @@
1
+ # rust_pgn_reader_python_binding
2
+ ## Fast PGN parsing bindings for Python
3
+ This project adds Python bindings to [rust-pgn-reader](https://github.com/niklasf/rust-pgn-reader). In addition, it also parses and extracts [%clk ..] and [%eval ..] tags from comments.
4
+
5
+ ## Installing
6
+ `pip install rust_pgn_reader_python_binding`
7
+
8
+ ## API
9
+
10
+ Three entry points are available:
11
+
12
+ - `parse_game(pgn)` - Parse a single PGN string
13
+ - `parse_games(chunked_array)` - Parse games from a PyArrow ChunkedArray (multithreaded)
14
+ - `parse_games_from_strings(pgns)` - Parse a list of PGN strings (multithreaded)
15
+
16
+ All return a `ParsedGames` container with flat NumPy arrays, supporting:
17
+ - Indexing (`result[i]`), slicing (`result[1:3]`), and iteration (`for game in result`)
18
+ - Per-game views (`PyGameView`) with zero-copy array slices
19
+ - Position-to-game and move-to-game mapping for ML workflows
20
+ - Optional comment storage (`store_comments=True`)
21
+ - Optional legal move storage (`store_legal_moves=True`)
22
+
23
+ ## Benchmarks
24
+ Below are some benchmarks on Lichess's 2013-07 chess games (293,459 games) on a 7800X3D.
25
+
26
+ | Parser | File format | Time |
27
+ |----------------------------------------------------------------------------|-------------|--------|
28
+ | [rust-pgn-reader](https://github.com/niklasf/rust-pgn-reader/tree/master) | PGN | 1s |
29
+ | rust_pgn_reader_python_binding, parse_games (multithreaded) | parquet | 0.35s |
30
+ | rust_pgn_reader_python_binding, parse_games_from_strings (multithreaded) | parquet | 0.5s |
31
+ | rust_pgn_reader_python_binding, parse_game (single-threaded) | parquet | 3.3s |
32
+ | rust_pgn_reader_python_binding, parse_game (single-threaded) | PGN | 4.7s |
33
+ | [chess-library](https://github.com/Disservin/chess-library) | PGN | 2s |
34
+ | [python-chess](https://github.com/niklasf/python-chess) | PGN | 3+ min |
35
+
36
+ To replicate, download `2013-07-train-00000-of-00001.parquet` and then run:
37
+
38
+ `python src/bench_parse_games.py` (recommended — multithreaded parse_games via Arrow)
39
+
40
+ `python src/bench_parse_games_from_strings.py` (multithreaded parse_games_from_strings)
41
+
42
+ `python src/bench_parse_game.py` (single-threaded parse_game from parquet)
43
+
44
+ `python src/bench_parse_game_pgn.py` (single-threaded parse_game from .pgn file)
45
+
46
+ `python src/bench_data_access.py 2013-07-train-00000-of-00001.parquet` (parsing + data access + memory)
47
+
48
+ ## Building
49
+ `maturin develop`
50
+
51
+ `maturin develop --release`
52
+
53
+ For a more thorough tutorial, follow https://lukesalamone.github.io/posts/how-to-create-rust-python-bindings/
54
+
55
+ ## Profiling
56
+ `py-spy record -s -F -f speedscope --output profile.speedscope -- python ./src/bench_parse_games.py`
57
+
58
+ Linux/WSL-only:
59
+ `py-spy record -s -F -n -f speedscope --output profile.speedscope -- python ./src/bench_parse_games.py`
60
+
61
+ ## Testing
62
+ `cargo test`
63
+
64
+ `python -m unittest src/test.py`
@@ -0,0 +1,154 @@
1
+ //! Benchmark for PGN parsing API, designed to mirror the Python workflow.
2
+ //!
3
+ //! `cargo bench --bench parquet_bench`
4
+ //! `samply record --rate 10000 cargo bench --bench parquet_bench`
5
+
6
+ use arrow::array::{Array, StringArray};
7
+ use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
8
+ use rayon::prelude::*;
9
+ use rayon::ThreadPoolBuilder;
10
+ use std::fs::File;
11
+ use std::path::Path;
12
+ use std::time::Instant;
13
+
14
+ use rust_pgn_reader_python_binding::{parse_game_to_buffers, Buffers, ParseConfig};
15
+
16
+ const FILE_PATH: &str = "2013-07-train-00000-of-00001.parquet";
17
+
18
+ /// Chunk multiplier for explicit chunking.
19
+ /// 1 = exactly num_threads chunks (minimal overhead)
20
+ /// Higher values provide better load balancing at cost of more buffers.
21
+ const CHUNK_MULTIPLIER: usize = 1;
22
+
23
+ /// Read parquet file and return the raw Arrow StringArrays.
24
+ /// This preserves Arrow's memory layout for zero-copy string access.
25
+ fn read_parquet_to_string_arrays(file_path: &str) -> Vec<StringArray> {
26
+ let file = File::open(Path::new(file_path)).expect("Unable to open file");
27
+ let builder = ParquetRecordBatchReaderBuilder::try_new(file)
28
+ .expect("Failed to create ParquetRecordBatchReaderBuilder");
29
+ let mut reader = builder
30
+ .build()
31
+ .expect("Failed to build ParquetRecordBatchReader");
32
+
33
+ let mut arrays = Vec::new();
34
+ while let Some(batch) = reader
35
+ .next()
36
+ .transpose()
37
+ .expect("Error reading record batch")
38
+ {
39
+ if let Some(array) = batch
40
+ .column_by_name("movetext")
41
+ .and_then(|col| col.as_any().downcast_ref::<StringArray>())
42
+ {
43
+ arrays.push(array.clone());
44
+ } else {
45
+ panic!("movetext column not found or not a StringArray");
46
+ }
47
+ }
48
+ arrays
49
+ }
50
+
51
+ /// Extract &str slices from Arrow StringArrays (zero-copy).
52
+ fn extract_str_slices<'a>(arrays: &'a [StringArray]) -> Vec<&'a str> {
53
+ let total_len: usize = arrays.iter().map(|a| a.len()).sum();
54
+ let mut slices = Vec::with_capacity(total_len);
55
+
56
+ for array in arrays {
57
+ for i in 0..array.len() {
58
+ if array.is_valid(i) {
59
+ slices.push(array.value(i));
60
+ }
61
+ }
62
+ }
63
+ slices
64
+ }
65
+
66
+ /// Benchmark the parsing API workflow.
67
+ ///
68
+ /// 1. Read parquet to Arrow arrays
69
+ /// 2. Extract &str slices from StringArray
70
+ /// 3. Parse in parallel with explicit chunking (par_chunks) -> fixed number of Buffers
71
+ ///
72
+ /// No merge step - the chunked architecture keeps per-thread buffers as-is.
73
+ pub fn bench_parse_api() {
74
+ let config = ParseConfig {
75
+ store_comments: false,
76
+ store_legal_moves: false,
77
+ };
78
+
79
+ // Step 1: Read parquet to Arrow StringArrays
80
+ let arrays = read_parquet_to_string_arrays(FILE_PATH);
81
+
82
+ // Step 2: Extract &str slices (zero-copy)
83
+ let pgn_slices = extract_str_slices(&arrays);
84
+ println!("Read {} games from parquet.", pgn_slices.len());
85
+
86
+ // Step 3: Build thread pool and compute capacity estimates
87
+ let num_threads = num_cpus::get();
88
+ let n_games = pgn_slices.len();
89
+ let moves_per_game = 70;
90
+
91
+ // Calculate chunk size for explicit chunking
92
+ let num_chunks = num_threads * CHUNK_MULTIPLIER;
93
+ let chunk_size = (n_games + num_chunks - 1) / num_chunks;
94
+ let chunk_size = chunk_size.max(1);
95
+ let games_per_chunk = chunk_size;
96
+
97
+ println!(
98
+ "Using {} threads, {} chunks, {} games/chunk",
99
+ num_threads, num_chunks, games_per_chunk
100
+ );
101
+
102
+ let thread_pool = ThreadPoolBuilder::new()
103
+ .num_threads(num_threads)
104
+ .build()
105
+ .expect("Failed to build Rayon thread pool");
106
+
107
+ // Step 4: Parse in parallel using par_chunks
108
+ let start = Instant::now();
109
+
110
+ let chunk_results: Vec<Buffers> = thread_pool.install(|| {
111
+ pgn_slices
112
+ .par_chunks(chunk_size)
113
+ .map(|chunk| {
114
+ let mut buffers = Buffers::with_capacity(games_per_chunk, moves_per_game, &config);
115
+ for &pgn in chunk {
116
+ let _ = parse_game_to_buffers(pgn, &mut buffers, &config);
117
+ }
118
+ buffers
119
+ })
120
+ .collect()
121
+ });
122
+
123
+ let duration_parallel = start.elapsed();
124
+ println!("Parallel parsing time: {:?}", duration_parallel);
125
+ println!(
126
+ "Created {} Buffers chunks (no merge needed)",
127
+ chunk_results.len()
128
+ );
129
+
130
+ // Compute totals from chunks
131
+ let total_games: usize = chunk_results.iter().map(|b| b.num_games()).sum();
132
+ let total_positions: usize = chunk_results.iter().map(|b| b.total_positions()).sum();
133
+
134
+ let duration_total = start.elapsed();
135
+ println!("Total time (parsing, no merge): {:?}", duration_total);
136
+ println!(
137
+ "Parsed {} games, {} total positions.",
138
+ total_games, total_positions
139
+ );
140
+
141
+ // Measure cleanup time
142
+ let drop_start = Instant::now();
143
+ drop(chunk_results);
144
+ let drop_duration = drop_start.elapsed();
145
+
146
+ let total_duration = start.elapsed();
147
+ println!("Cleanup time (drop): {:?}", drop_duration);
148
+ println!("Total time (parsing + cleanup): {:?}", total_duration);
149
+ }
150
+
151
+ fn main() {
152
+ println!("=== Parse API (Buffers) ===\n");
153
+ bench_parse_api();
154
+ }