python-calamine-reducto 0.7.18__tar.gz → 0.7.19__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.
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/Cargo.lock +1 -1
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/Cargo.toml +1 -1
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/PKG-INFO +1 -1
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/python/python_calamine/_python_calamine.pyi +12 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/grid.rs +99 -23
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/workbook.rs +499 -67
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_band.py +18 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_bounded_reads.py +13 -0
- python_calamine_reducto-0.7.19/tests/test_gap_compression.py +258 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/.github/workflows/CI.yml +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/.github/workflows/release.yml +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/.gitignore +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/.pre-commit-config.yaml +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/.python-version +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/CLAUDE.md +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/LICENSE +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/README.md +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/build.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/pyproject.toml +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/python/python_calamine/__init__.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/python/python_calamine/py.typed +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/renovate.json +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/lib.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/cell.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/errors.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/mod.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/sheet.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/src/types/table.rs +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/__init__.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/conftest.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/create_empty_merge_fixture.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/create_hidden_col_range_fixture.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/create_richtext_fixture.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/create_test_fixture.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/any_sheets.ods +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/any_sheets.xls +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/any_sheets.xlsb +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/any_sheets.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/base.ods +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/base.xls +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/base.xlsb +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/base.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/dropdown_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/empty_file.ods +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/empty_file.xlsb +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/empty_file.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/empty_merge_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/formula_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/grid_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/hidden_col_range_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/image_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/issue139.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/multi_image_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/password.ods +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/password.xls +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/password.xlsb +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/password.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/richtext_test.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/table-multiple.xlsb +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/table-multiple.xlsx +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/data/temperature-table.xlsb +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_base.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_grid.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_images.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_pandas.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_sheet_type.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_tables.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_trim_header_only.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/test_uncached_formulas.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/tests/xlsx_builder.py +0 -0
- {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.19}/uv.lock +0 -0
|
@@ -218,6 +218,14 @@ class CellGrid:
|
|
|
218
218
|
def shape(self) -> tuple[int, int]: ...
|
|
219
219
|
@property
|
|
220
220
|
def size(self) -> int: ...
|
|
221
|
+
@property
|
|
222
|
+
def row_coords(self) -> npt.NDArray[np.int32]:
|
|
223
|
+
"""Grid row -> 0-based sheet row: `arange` plus the band top, unless gap compression engaged."""
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def col_coords(self) -> npt.NDArray[np.int32]:
|
|
227
|
+
"""Grid column -> 0-based sheet column."""
|
|
228
|
+
|
|
221
229
|
@property
|
|
222
230
|
def uncached_formula_count(self) -> int | None:
|
|
223
231
|
"""Formula cells with no cached value (streaming xlsx grids), else `None`."""
|
|
@@ -385,10 +393,14 @@ class CalamineWorkbook:
|
|
|
385
393
|
trigger_density: float = 0.001,
|
|
386
394
|
coverage: float = 0.999,
|
|
387
395
|
trim_header_only: bool = False,
|
|
396
|
+
max_empty_run: int | None = None,
|
|
388
397
|
) -> tuple[CellGrid, int, int, int]:
|
|
389
398
|
"""Stream a grid. Returns `(grid, declared_rows, declared_cols, dropped_cells)`.
|
|
390
399
|
|
|
391
400
|
`trim_header_only` also drops the trailing columns whose only value is in the header row.
|
|
401
|
+
`max_empty_run` keeps at most that many rows (and columns) of each empty run inside the
|
|
402
|
+
trimmed bounds. It engages only above `trigger_cells`, and only when it halves the grid.
|
|
403
|
+
`coord` and `merge_parent` stay in sheet positions; `row_coords`/`col_coords` map them.
|
|
392
404
|
"""
|
|
393
405
|
|
|
394
406
|
def get_sheet_filled_count(self, name: str) -> tuple[int, int, int]:
|
|
@@ -185,7 +185,8 @@ pub struct CellGrid {
|
|
|
185
185
|
// Styles (None when styling skipped for large sheets)
|
|
186
186
|
pub(crate) styles: Option<CellStyles>,
|
|
187
187
|
|
|
188
|
-
// Merge map: (row, col) -> (parent_row, parent_col),
|
|
188
|
+
// Merge map: (row, col) -> (parent_row, parent_col), both grid indexes. An anchor
|
|
189
|
+
// above the grid (a band) is negative. `anchor_position` maps it for `merge_parent`.
|
|
189
190
|
pub(crate) merge_map: HashMap<(usize, usize), (i32, i32)>,
|
|
190
191
|
|
|
191
192
|
// Mutable boolean fields
|
|
@@ -211,6 +212,14 @@ pub struct CellGrid {
|
|
|
211
212
|
pub(crate) coord_row_offset: usize,
|
|
212
213
|
pub(crate) coord_col_offset: usize,
|
|
213
214
|
|
|
215
|
+
// Added to a merge anchor for `merge_parent` (the band top for `propagate_merges`).
|
|
216
|
+
pub(crate) anchor_offset: (i32, i32),
|
|
217
|
+
|
|
218
|
+
// Gap compression: grid row/col -> 0-based sheet row/col, ascending. When set,
|
|
219
|
+
// `coord` and `merge_parent` use them and the offsets are 0.
|
|
220
|
+
pub(crate) row_coords: Option<Vec<i32>>,
|
|
221
|
+
pub(crate) col_coords: Option<Vec<i32>>,
|
|
222
|
+
|
|
214
223
|
// Formula cells with no cached value in the source sheet. None when unknown
|
|
215
224
|
// (non-xlsx, dense path, or a derived grid).
|
|
216
225
|
pub(crate) uncached_formula_count: Option<u64>,
|
|
@@ -259,6 +268,9 @@ impl CellGrid {
|
|
|
259
268
|
merge_parent_default,
|
|
260
269
|
coord_row_offset: 0,
|
|
261
270
|
coord_col_offset: 0,
|
|
271
|
+
anchor_offset: (0, 0),
|
|
272
|
+
row_coords: None,
|
|
273
|
+
col_coords: None,
|
|
262
274
|
uncached_formula_count: None,
|
|
263
275
|
}
|
|
264
276
|
}
|
|
@@ -280,6 +292,38 @@ impl CellGrid {
|
|
|
280
292
|
row * self.cols + col
|
|
281
293
|
}
|
|
282
294
|
|
|
295
|
+
/// The 0-based sheet row of a grid row.
|
|
296
|
+
#[inline]
|
|
297
|
+
fn sheet_row(&self, row: usize) -> usize {
|
|
298
|
+
match &self.row_coords {
|
|
299
|
+
Some(coords) => coords[row] as usize,
|
|
300
|
+
None => row + self.coord_row_offset,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// The 0-based sheet column of a grid column.
|
|
305
|
+
#[inline]
|
|
306
|
+
fn sheet_col(&self, col: usize) -> usize {
|
|
307
|
+
match &self.col_coords {
|
|
308
|
+
Some(coords) => coords[col] as usize,
|
|
309
|
+
None => col + self.coord_col_offset,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// The flat index of a merge anchor, `None` when the anchor is outside the grid.
|
|
314
|
+
fn anchor_index(&self, pr: i32, pc: i32) -> Option<usize> {
|
|
315
|
+
let (row, col) = (usize::try_from(pr).ok()?, usize::try_from(pc).ok()?);
|
|
316
|
+
(row < self.rows && col < self.cols).then(|| self.idx(row, col))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// The `merge_parent` of a merge anchor. A compressed grid keeps every anchor.
|
|
320
|
+
fn anchor_position(&self, pr: i32, pc: i32) -> (i32, i32) {
|
|
321
|
+
match (&self.row_coords, &self.col_coords) {
|
|
322
|
+
(Some(rows), Some(cols)) => (rows[pr as usize], cols[pc as usize]),
|
|
323
|
+
_ => (pr + self.anchor_offset.0, pc + self.anchor_offset.1),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
283
327
|
/// `fill_numpy_array` empties the per-cell storage (`header` first). A later
|
|
284
328
|
/// cell access would index an empty `Vec`, so refuse it.
|
|
285
329
|
fn ensure_not_drained(&self) -> PyResult<()> {
|
|
@@ -324,7 +368,7 @@ impl CellGrid {
|
|
|
324
368
|
// Both parent and child cells — openpyxl uses value emptiness, not coordinates.
|
|
325
369
|
match self.merge_map.get(&(row, col)) {
|
|
326
370
|
Some(&(pr, pc)) if matches!(self.raw_values[self.idx(row, col)], RawValue::Empty) => {
|
|
327
|
-
(pr, pc)
|
|
371
|
+
self.anchor_position(pr, pc)
|
|
328
372
|
}
|
|
329
373
|
_ => self.merge_parent_default,
|
|
330
374
|
}
|
|
@@ -368,8 +412,8 @@ impl CellGrid {
|
|
|
368
412
|
}
|
|
369
413
|
"coord" => {
|
|
370
414
|
let arr = [
|
|
371
|
-
(
|
|
372
|
-
(
|
|
415
|
+
(self.sheet_row(row) + 1) as i32,
|
|
416
|
+
(self.sheet_col(col) + 1) as i32,
|
|
373
417
|
];
|
|
374
418
|
let numpy = py.import("numpy")?;
|
|
375
419
|
let result = numpy.call_method1("array", (arr.to_vec(), "int32"))?;
|
|
@@ -508,15 +552,8 @@ impl CellGrid {
|
|
|
508
552
|
// For merged cells, use parent's style
|
|
509
553
|
let (row, col) = (idx / self.cols, idx % self.cols);
|
|
510
554
|
let style_idx = match self.merge_map.get(&(row, col)) {
|
|
511
|
-
Some(&(pr, pc))
|
|
512
|
-
|
|
513
|
-
if pidx < total {
|
|
514
|
-
pidx
|
|
515
|
-
} else {
|
|
516
|
-
idx
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
_ => idx,
|
|
555
|
+
Some(&(pr, pc)) => self.anchor_index(pr, pc).unwrap_or(idx),
|
|
556
|
+
None => idx,
|
|
520
557
|
};
|
|
521
558
|
let id = styles.ids[style_idx];
|
|
522
559
|
let (font, fill) = match names.entry(id) {
|
|
@@ -562,26 +599,43 @@ impl CellGrid {
|
|
|
562
599
|
if r < self.rows && c < self.cols {
|
|
563
600
|
let idx = r * self.cols + c;
|
|
564
601
|
if self.raw_values[idx].is_empty() {
|
|
565
|
-
data[idx * 2] = pr;
|
|
566
|
-
data[idx * 2 + 1] = pc;
|
|
602
|
+
(data[idx * 2], data[idx * 2 + 1]) = self.anchor_position(pr, pc);
|
|
567
603
|
}
|
|
568
604
|
}
|
|
569
605
|
}
|
|
570
606
|
data
|
|
571
607
|
}
|
|
572
608
|
|
|
573
|
-
/// `coord` as flat 1-based `(row, col)`
|
|
609
|
+
/// `coord` as flat 1-based `(row, col)` sheet positions.
|
|
574
610
|
fn coord_pairs(&self) -> Vec<i32> {
|
|
611
|
+
let col_coords: Vec<i32> = (0..self.cols)
|
|
612
|
+
.map(|c| (self.sheet_col(c) + 1) as i32)
|
|
613
|
+
.collect();
|
|
575
614
|
let mut data = Vec::with_capacity(self.rows * self.cols * 2);
|
|
576
615
|
for r in 0..self.rows {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
data.push(
|
|
616
|
+
let row = (self.sheet_row(r) + 1) as i32;
|
|
617
|
+
for &col in &col_coords {
|
|
618
|
+
data.push(row);
|
|
619
|
+
data.push(col);
|
|
580
620
|
}
|
|
581
621
|
}
|
|
582
622
|
data
|
|
583
623
|
}
|
|
584
624
|
|
|
625
|
+
/// 0-based sheet indexes of the grid rows or columns, as a numpy `int32` array:
|
|
626
|
+
/// a copy of the compressed `coords`, else `offset..offset + n`.
|
|
627
|
+
fn sheet_coords<'py>(
|
|
628
|
+
py: Python<'py>,
|
|
629
|
+
coords: Option<&[i32]>,
|
|
630
|
+
n: usize,
|
|
631
|
+
offset: usize,
|
|
632
|
+
) -> Bound<'py, PyArray1<i32>> {
|
|
633
|
+
match coords {
|
|
634
|
+
Some(coords) => PyArray1::from_slice(py, coords),
|
|
635
|
+
None => PyArray1::from_iter(py, (offset..offset + n).map(|i| i as i32)),
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
585
639
|
fn font_bolds(&self) -> Vec<bool> {
|
|
586
640
|
match &self.styles {
|
|
587
641
|
Some(styles) => styles
|
|
@@ -752,6 +806,29 @@ impl CellGrid {
|
|
|
752
806
|
self.rows * self.cols
|
|
753
807
|
}
|
|
754
808
|
|
|
809
|
+
/// Grid row -> 0-based sheet row: `arange` plus the coord offset (the band top),
|
|
810
|
+
/// unless gap compression engaged.
|
|
811
|
+
#[getter]
|
|
812
|
+
fn row_coords<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<i32>> {
|
|
813
|
+
Self::sheet_coords(
|
|
814
|
+
py,
|
|
815
|
+
self.row_coords.as_deref(),
|
|
816
|
+
self.rows,
|
|
817
|
+
self.coord_row_offset,
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/// Grid column -> 0-based sheet column, like `row_coords`.
|
|
822
|
+
#[getter]
|
|
823
|
+
fn col_coords<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<i32>> {
|
|
824
|
+
Self::sheet_coords(
|
|
825
|
+
py,
|
|
826
|
+
self.col_coords.as_deref(),
|
|
827
|
+
self.cols,
|
|
828
|
+
self.coord_col_offset,
|
|
829
|
+
)
|
|
830
|
+
}
|
|
831
|
+
|
|
755
832
|
/// Formula cells with no cached value, counted by `get_sheet_grid_streaming`.
|
|
756
833
|
/// `None` when unknown (non-xlsx workbook or a grid from another builder).
|
|
757
834
|
#[getter]
|
|
@@ -1110,7 +1187,7 @@ impl CellGrid {
|
|
|
1110
1187
|
self.merge_map.iter().map(|(&k, &v)| (k, v)).collect();
|
|
1111
1188
|
for ((row, col), (pr, pc)) in entries {
|
|
1112
1189
|
// Skip if this IS the parent
|
|
1113
|
-
if pr == row as i32
|
|
1190
|
+
if (pr, pc) == (row as i32, col as i32) {
|
|
1114
1191
|
continue;
|
|
1115
1192
|
}
|
|
1116
1193
|
let idx = row * self.cols + col;
|
|
@@ -1119,9 +1196,8 @@ impl CellGrid {
|
|
|
1119
1196
|
.as_ref()
|
|
1120
1197
|
.map(|s| s.is_empty())
|
|
1121
1198
|
.unwrap_or(true);
|
|
1122
|
-
if child_empty
|
|
1123
|
-
let parent_idx =
|
|
1124
|
-
if parent_idx < self.formatted_values.len() {
|
|
1199
|
+
if child_empty {
|
|
1200
|
+
if let Some(parent_idx) = self.anchor_index(pr, pc) {
|
|
1125
1201
|
self.formatted_values[idx] = self.formatted_values[parent_idx].clone();
|
|
1126
1202
|
}
|
|
1127
1203
|
}
|