python-calamine-reducto 0.7.19__tar.gz → 0.7.20__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.19 → python_calamine_reducto-0.7.20}/Cargo.lock +2 -2
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/Cargo.toml +1 -1
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/PKG-INFO +1 -1
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/README.md +2 -1
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/python/python_calamine/_python_calamine.pyi +28 -1
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/workbook.rs +85 -12
- python_calamine_reducto-0.7.20/tests/create_hyperlink_fixture.py +30 -0
- python_calamine_reducto-0.7.20/tests/data/hyperlink_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_images.py +83 -2
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/.github/workflows/CI.yml +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/.github/workflows/release.yml +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/.gitignore +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/.pre-commit-config.yaml +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/.python-version +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/CLAUDE.md +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/LICENSE +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/build.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/pyproject.toml +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/python/python_calamine/__init__.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/python/python_calamine/py.typed +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/renovate.json +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/lib.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/cell.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/errors.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/grid.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/mod.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/sheet.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/src/types/table.rs +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/__init__.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/conftest.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/create_empty_merge_fixture.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/create_hidden_col_range_fixture.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/create_richtext_fixture.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/create_test_fixture.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.ods +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xls +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xlsb +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/base.ods +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/base.xls +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/base.xlsb +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/base.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/dropdown_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_file.ods +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_file.xlsb +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_file.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_merge_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/formula_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/grid_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/hidden_col_range_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/image_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/issue139.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/multi_image_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/password.ods +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/password.xls +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/password.xlsb +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/password.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/richtext_test.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/table-multiple.xlsb +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/table-multiple.xlsx +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/temperature-table.xlsb +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_band.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_base.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_bounded_reads.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_gap_compression.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_grid.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_pandas.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_sheet_type.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_tables.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_trim_header_only.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_uncached_formulas.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/xlsx_builder.py +0 -0
- {python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/uv.lock +0 -0
|
@@ -80,7 +80,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|
|
80
80
|
[[package]]
|
|
81
81
|
name = "calamine"
|
|
82
82
|
version = "0.32.0"
|
|
83
|
-
source = "git+https://github.com/reductoai/calamine.git?branch=main#
|
|
83
|
+
source = "git+https://github.com/reductoai/calamine.git?branch=main#5a6418cb25f78e47a841f51c42c85363ee2cd06b"
|
|
84
84
|
dependencies = [
|
|
85
85
|
"atoi_simd",
|
|
86
86
|
"byteorder",
|
|
@@ -612,7 +612,7 @@ dependencies = [
|
|
|
612
612
|
|
|
613
613
|
[[package]]
|
|
614
614
|
name = "python-calamine-reducto"
|
|
615
|
-
version = "0.7.
|
|
615
|
+
version = "0.7.20"
|
|
616
616
|
dependencies = [
|
|
617
617
|
"calamine",
|
|
618
618
|
"chrono",
|
|
@@ -25,7 +25,8 @@ Our additions live on `main`.
|
|
|
25
25
|
- **`get_sheet_grid(name, rows, cols, include_styles=True)`** → `CellGrid` from the dense range.
|
|
26
26
|
- **`get_sheet_layout(name)`** → dict with row heights, column widths, defaults
|
|
27
27
|
- **`get_sheet_formulas`**, **`get_sheet_merged_ranges`**, **`get_sheet_images`**,
|
|
28
|
-
**`get_sheet_data_validations`**, **`
|
|
28
|
+
**`get_sheet_data_validations`**, **`get_sheet_hyperlinks`**, **`get_sheet_hyperlink_formulas`**,
|
|
29
|
+
**`get_sheet_freeze_panes`**,
|
|
29
30
|
**`get_sheet_filled_count`**, **`get_sheet_uncached_formula_count`**,
|
|
30
31
|
**`get_all_tables_metadata`**
|
|
31
32
|
|
|
@@ -204,6 +204,21 @@ class DataValidation(typing.TypedDict):
|
|
|
204
204
|
formula2: str | None
|
|
205
205
|
allow_blank: bool
|
|
206
206
|
|
|
207
|
+
@typing.type_check_only
|
|
208
|
+
class Hyperlink(typing.TypedDict):
|
|
209
|
+
ref: str
|
|
210
|
+
"""Cell or range reference, e.g. `A1` or `A1:C3`."""
|
|
211
|
+
rel_id: str | None
|
|
212
|
+
"""The `r:id` of the link, if any."""
|
|
213
|
+
target: str | None
|
|
214
|
+
"""Relationship target, e.g. a URL. `None` for a link with only a `location`, and for an
|
|
215
|
+
`r:id` that has no hyperlink relationship."""
|
|
216
|
+
location: str | None
|
|
217
|
+
"""Place inside the target or the workbook, e.g. `Sheet2!A1` or a URL fragment."""
|
|
218
|
+
display: str | None
|
|
219
|
+
external: bool
|
|
220
|
+
"""`True` when the relationship has `TargetMode="External"`."""
|
|
221
|
+
|
|
207
222
|
@typing.final
|
|
208
223
|
class CellProxy:
|
|
209
224
|
"""One cell of a `CellGrid`. Read a field with `cell["value"]`."""
|
|
@@ -261,7 +276,9 @@ class CellGrid:
|
|
|
261
276
|
def apply_formatting_rust(self) -> None:
|
|
262
277
|
"""Format each value with its number format (SSF, in Rust)."""
|
|
263
278
|
|
|
264
|
-
def apply_dateutil_reformat(
|
|
279
|
+
def apply_dateutil_reformat(
|
|
280
|
+
self, date_parse: Callable[[str], float | None]
|
|
281
|
+
) -> None:
|
|
265
282
|
"""Reformat text cells that have a date format. `date_parse` gives a date serial."""
|
|
266
283
|
|
|
267
284
|
def apply_raw_as_formatted(self) -> None: ...
|
|
@@ -428,6 +445,16 @@ class CalamineWorkbook:
|
|
|
428
445
|
"""`(data, extension, anchor_row, anchor_col)` for each image."""
|
|
429
446
|
|
|
430
447
|
def get_sheet_data_validations(self, name: str) -> list[DataValidation]: ...
|
|
448
|
+
def get_sheet_hyperlinks(
|
|
449
|
+
self, name: str, limit: int | None = None
|
|
450
|
+
) -> list[Hyperlink]:
|
|
451
|
+
"""The links of the sheet's `<hyperlinks>` element, at most `limit`. Empty for xlsb, xls, and ods."""
|
|
452
|
+
|
|
453
|
+
def get_sheet_hyperlink_formulas(
|
|
454
|
+
self, name: str, limit: int | None = None
|
|
455
|
+
) -> list[tuple[int, int, str]]:
|
|
456
|
+
"""`(row, col, formula)` for each formula that contains `HYPERLINK`, at most `limit`. Empty for xlsb, xls, and ods."""
|
|
457
|
+
|
|
431
458
|
def get_sheet_freeze_panes(self, name: str) -> tuple[int, int]:
|
|
432
459
|
"""`(frozen_rows, frozen_cols)`."""
|
|
433
460
|
|
|
@@ -133,6 +133,14 @@ impl SheetsEnum {
|
|
|
133
133
|
with_xlsx!(self, x => Ok(x.worksheet_data_validations(name)?), else Ok(Vec::new()))
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
fn worksheet_hyperlinks(
|
|
137
|
+
&mut self,
|
|
138
|
+
name: &str,
|
|
139
|
+
limit: Option<usize>,
|
|
140
|
+
) -> Result<Vec<calamine::Hyperlink>, Error> {
|
|
141
|
+
with_xlsx!(self, x => Ok(x.worksheet_hyperlinks(name, limit)?), else Ok(Vec::new()))
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
fn worksheet_freeze_panes(&mut self, name: &str) -> Result<calamine::FreezePanes, Error> {
|
|
137
145
|
with_xlsx!(
|
|
138
146
|
self,
|
|
@@ -183,10 +191,14 @@ impl SheetsEnum {
|
|
|
183
191
|
}, else Ok(None))
|
|
184
192
|
}
|
|
185
193
|
|
|
186
|
-
/// Stream the formulas
|
|
187
|
-
///
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
/// Stream the xlsx formulas that `keep` accepts, at most `limit`, with no dense `Range<String>`.
|
|
195
|
+
/// Keyed by position, the last duplicate kept, as `worksheet_formula` gives them. `Ok(None)` for non-xlsx.
|
|
196
|
+
fn xlsx_formulas(
|
|
197
|
+
&mut self,
|
|
198
|
+
name: &str,
|
|
199
|
+
keep: fn(&str) -> bool,
|
|
200
|
+
limit: usize,
|
|
201
|
+
) -> Result<Option<Formulas>, Error> {
|
|
190
202
|
with_xlsx!(self, x => {
|
|
191
203
|
let mut formulas = BTreeMap::new();
|
|
192
204
|
let mut reader = match x.worksheet_cells_reader(name) {
|
|
@@ -194,9 +206,13 @@ impl SheetsEnum {
|
|
|
194
206
|
Err(calamine::XlsxError::NotAWorksheet(_)) => return Ok(Some(formulas)),
|
|
195
207
|
Err(e) => return Err(e.into()),
|
|
196
208
|
};
|
|
197
|
-
while
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
while formulas.len() < limit {
|
|
210
|
+
let Some(cell) = reader.next_formula()? else {
|
|
211
|
+
break;
|
|
212
|
+
};
|
|
213
|
+
let formula = cell.get_value();
|
|
214
|
+
if !formula.is_empty() && keep(formula) {
|
|
215
|
+
formulas.insert(cell.get_position(), formula.clone());
|
|
200
216
|
}
|
|
201
217
|
}
|
|
202
218
|
Ok(Some(formulas))
|
|
@@ -577,6 +593,50 @@ impl CalamineWorkbook {
|
|
|
577
593
|
.collect()
|
|
578
594
|
}
|
|
579
595
|
|
|
596
|
+
/// Get the hyperlinks of a sheet as a list of dicts, at most `limit` of them.
|
|
597
|
+
/// Each dict has: ref, rel_id, target, location, display, external. Empty for xlsb, xls, and ods.
|
|
598
|
+
#[pyo3(name = "get_sheet_hyperlinks", signature = (name, limit = None))]
|
|
599
|
+
fn py_get_sheet_hyperlinks(
|
|
600
|
+
&mut self,
|
|
601
|
+
py: Python<'_>,
|
|
602
|
+
name: &str,
|
|
603
|
+
limit: Option<usize>,
|
|
604
|
+
) -> PyResult<Vec<Py<PyAny>>> {
|
|
605
|
+
let links = py.detach(|| -> PyResult<Vec<calamine::Hyperlink>> {
|
|
606
|
+
Ok(self.sheets.worksheet_hyperlinks(name, limit)?)
|
|
607
|
+
})?;
|
|
608
|
+
links
|
|
609
|
+
.into_iter()
|
|
610
|
+
.map(|link| {
|
|
611
|
+
let dict = pyo3::types::PyDict::new(py);
|
|
612
|
+
dict.set_item("ref", &link.cell_ref)?;
|
|
613
|
+
dict.set_item("rel_id", link.rel_id.as_deref())?;
|
|
614
|
+
dict.set_item("target", link.target.as_deref())?;
|
|
615
|
+
dict.set_item("location", link.location.as_deref())?;
|
|
616
|
+
dict.set_item("display", link.display.as_deref())?;
|
|
617
|
+
dict.set_item("external", link.external)?;
|
|
618
|
+
Ok(dict.into_any().unbind())
|
|
619
|
+
})
|
|
620
|
+
.collect()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Get `(row, col, formula)` for each formula that contains `HYPERLINK` (any case), at most
|
|
624
|
+
/// `limit` of them. The read streams the cells. Empty for xlsb, xls, and ods.
|
|
625
|
+
#[pyo3(name = "get_sheet_hyperlink_formulas", signature = (name, limit = None))]
|
|
626
|
+
fn py_get_sheet_hyperlink_formulas(
|
|
627
|
+
&mut self,
|
|
628
|
+
py: Python<'_>,
|
|
629
|
+
name: &str,
|
|
630
|
+
limit: Option<usize>,
|
|
631
|
+
) -> PyResult<Vec<(usize, usize, String)>> {
|
|
632
|
+
py.detach(|| {
|
|
633
|
+
let formulas =
|
|
634
|
+
self.sheets
|
|
635
|
+
.xlsx_formulas(name, contains_hyperlink, limit.unwrap_or(usize::MAX))?;
|
|
636
|
+
Ok(formula_triples(formulas.unwrap_or_default()))
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
580
640
|
/// Get freeze pane info for a sheet as (frozen_rows, frozen_cols).
|
|
581
641
|
#[pyo3(name = "get_sheet_freeze_panes")]
|
|
582
642
|
fn py_get_sheet_freeze_panes(&mut self, py: Python<'_>, name: &str) -> PyResult<(u32, u32)> {
|
|
@@ -743,11 +803,8 @@ impl CalamineWorkbook {
|
|
|
743
803
|
|
|
744
804
|
/// Get formulas as (row, col, formula_string) triples.
|
|
745
805
|
fn get_sheet_formulas(&mut self, name: &str) -> PyResult<Vec<(usize, usize, String)>> {
|
|
746
|
-
if let Some(formulas) = self.sheets.xlsx_formulas(name)? {
|
|
747
|
-
return Ok(formulas
|
|
748
|
-
.into_iter()
|
|
749
|
-
.map(|((row, col), f)| (row as usize, col as usize, with_leading_eq(f)))
|
|
750
|
-
.collect());
|
|
806
|
+
if let Some(formulas) = self.sheets.xlsx_formulas(name, |_| true, usize::MAX)? {
|
|
807
|
+
return Ok(formula_triples(formulas));
|
|
751
808
|
}
|
|
752
809
|
let range = self.sheets.worksheet_formula(name)?;
|
|
753
810
|
let range_start = range.start().unwrap_or((0, 0));
|
|
@@ -1740,6 +1797,22 @@ fn coverage_cut(fill: &HashMap<usize, u64>, max: usize, filled: u64, coverage: f
|
|
|
1740
1797
|
max
|
|
1741
1798
|
}
|
|
1742
1799
|
|
|
1800
|
+
/// `(row, col, formula)` for each formula, each with a leading `=`.
|
|
1801
|
+
fn formula_triples(formulas: Formulas) -> Vec<(usize, usize, String)> {
|
|
1802
|
+
formulas
|
|
1803
|
+
.into_iter()
|
|
1804
|
+
.map(|((row, col), f)| (row as usize, col as usize, with_leading_eq(f)))
|
|
1805
|
+
.collect()
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/// Whether a formula contains `HYPERLINK` in any letter case.
|
|
1809
|
+
fn contains_hyperlink(formula: &str) -> bool {
|
|
1810
|
+
formula
|
|
1811
|
+
.as_bytes()
|
|
1812
|
+
.windows(b"HYPERLINK".len())
|
|
1813
|
+
.any(|w| w.eq_ignore_ascii_case(b"HYPERLINK"))
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1743
1816
|
/// Calamine formula strings have no leading `=`, but the API expects one.
|
|
1744
1817
|
fn with_leading_eq(formula: String) -> String {
|
|
1745
1818
|
if formula.starts_with('=') {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Write tests/data/hyperlink_test.xlsx: links, an internal link, HYPERLINK formulas, and a sheet with no links.
|
|
2
|
+
Run once: python tests/create_hyperlink_fixture.py"""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import openpyxl
|
|
7
|
+
from openpyxl.worksheet.hyperlink import Hyperlink
|
|
8
|
+
|
|
9
|
+
wb = openpyxl.Workbook()
|
|
10
|
+
|
|
11
|
+
ws = wb.active
|
|
12
|
+
ws.title = "Links"
|
|
13
|
+
ws["A1"] = "Reducto"
|
|
14
|
+
ws["A1"].hyperlink = "https://reducto.ai/"
|
|
15
|
+
ws["A2"] = "Search"
|
|
16
|
+
ws["A2"].hyperlink = "https://example.com/search?q=a&page=2"
|
|
17
|
+
ws["A3"] = "Go to Other"
|
|
18
|
+
ws["A3"].hyperlink = Hyperlink(ref="A3", location="Other!B2", display="Go to Other")
|
|
19
|
+
ws["B1"] = "no link"
|
|
20
|
+
ws["C1"] = '=HYPERLINK("https://example.com/f","F")'
|
|
21
|
+
ws["C2"] = "=SUM(1,2)"
|
|
22
|
+
ws["C3"] = '=hyperlink("https://example.com/g")'
|
|
23
|
+
|
|
24
|
+
other = wb.create_sheet("Other")
|
|
25
|
+
other["B2"] = "target"
|
|
26
|
+
other["A1"] = "=SUM(1,2)"
|
|
27
|
+
|
|
28
|
+
out = Path(__file__).parent / "data" / "hyperlink_test.xlsx"
|
|
29
|
+
wb.save(out)
|
|
30
|
+
print(f"Created {out}")
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for native image, formula,
|
|
1
|
+
"""Tests for native image, formula, data validation, and hyperlink extraction via calamine bindings.
|
|
2
2
|
|
|
3
3
|
Uses pre-built xlsx fixtures in tests/data/ (created with openpyxl).
|
|
4
4
|
"""
|
|
@@ -6,7 +6,6 @@ Uses pre-built xlsx fixtures in tests/data/ (created with openpyxl).
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
|
-
|
|
10
9
|
from python_calamine import CalamineWorkbook
|
|
11
10
|
|
|
12
11
|
DATA = Path(__file__).parent / "data"
|
|
@@ -14,6 +13,7 @@ IMAGE_TEST = DATA / "image_test.xlsx"
|
|
|
14
13
|
MULTI_IMAGE_TEST = DATA / "multi_image_test.xlsx"
|
|
15
14
|
FORMULA_TEST = DATA / "formula_test.xlsx"
|
|
16
15
|
DROPDOWN_TEST = DATA / "dropdown_test.xlsx"
|
|
16
|
+
HYPERLINK_TEST = DATA / "hyperlink_test.xlsx"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class TestGetSheetImages:
|
|
@@ -126,3 +126,84 @@ class TestGetSheetDataValidations:
|
|
|
126
126
|
wb = CalamineWorkbook.from_path(IMAGE_TEST)
|
|
127
127
|
dvs = wb.get_sheet_data_validations("Images")
|
|
128
128
|
assert dvs == []
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestGetSheetHyperlinks:
|
|
132
|
+
def test_returns_external_and_internal_links(self) -> None:
|
|
133
|
+
"""Catches a lost target, an escaped `&` in a URL, or a dropped location-only link."""
|
|
134
|
+
wb = CalamineWorkbook.from_path(HYPERLINK_TEST)
|
|
135
|
+
links = wb.get_sheet_hyperlinks("Links")
|
|
136
|
+
|
|
137
|
+
assert links == [
|
|
138
|
+
{
|
|
139
|
+
"ref": "A1",
|
|
140
|
+
"rel_id": "rId1",
|
|
141
|
+
"target": "https://reducto.ai/",
|
|
142
|
+
"location": None,
|
|
143
|
+
"display": None,
|
|
144
|
+
"external": True,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"ref": "A2",
|
|
148
|
+
"rel_id": "rId2",
|
|
149
|
+
"target": "https://example.com/search?q=a&page=2",
|
|
150
|
+
"location": None,
|
|
151
|
+
"display": None,
|
|
152
|
+
"external": True,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"ref": "A3",
|
|
156
|
+
"rel_id": None,
|
|
157
|
+
"target": None,
|
|
158
|
+
"location": "Other!B2",
|
|
159
|
+
"display": "Go to Other",
|
|
160
|
+
"external": False,
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
def test_limit_stops_the_read(self) -> None:
|
|
165
|
+
"""Catches a limit that the reader ignores, which lets a hostile sheet fill the memory."""
|
|
166
|
+
wb = CalamineWorkbook.from_path(HYPERLINK_TEST)
|
|
167
|
+
links = wb.get_sheet_hyperlinks("Links", limit=2)
|
|
168
|
+
assert [link["ref"] for link in links] == ["A1", "A2"]
|
|
169
|
+
|
|
170
|
+
def test_sheet_without_links_returns_empty(self) -> None:
|
|
171
|
+
"""Catches links of one sheet that leak into another sheet."""
|
|
172
|
+
wb = CalamineWorkbook.from_path(HYPERLINK_TEST)
|
|
173
|
+
assert wb.get_sheet_hyperlinks("Other") == []
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize(
|
|
176
|
+
"name", ["any_sheets.xlsb", "any_sheets.ods", "any_sheets.xls"]
|
|
177
|
+
)
|
|
178
|
+
def test_non_xlsx_returns_empty(self, name: str) -> None:
|
|
179
|
+
"""Catches an error, not an empty list, for formats with no hyperlink reader."""
|
|
180
|
+
wb = CalamineWorkbook.from_path(DATA / name)
|
|
181
|
+
assert wb.get_sheet_hyperlinks(wb.sheet_names[0]) == []
|
|
182
|
+
assert wb.get_sheet_hyperlink_formulas(wb.sheet_names[0]) == []
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestGetSheetHyperlinkFormulas:
|
|
186
|
+
def test_returns_only_hyperlink_formulas_in_any_case(self) -> None:
|
|
187
|
+
"""Catches other formulas that cross into Python, or a lower-case `hyperlink(` that is lost."""
|
|
188
|
+
wb = CalamineWorkbook.from_path(HYPERLINK_TEST)
|
|
189
|
+
assert wb.get_sheet_hyperlink_formulas("Links") == [
|
|
190
|
+
(0, 2, '=HYPERLINK("https://example.com/f","F")'),
|
|
191
|
+
(2, 2, '=hyperlink("https://example.com/g")'),
|
|
192
|
+
]
|
|
193
|
+
assert wb.get_sheet_hyperlink_formulas("Other") == []
|
|
194
|
+
|
|
195
|
+
def test_limit_stops_the_read(self) -> None:
|
|
196
|
+
"""Catches a limit that the reader ignores, which lets a formula-heavy sheet fill the memory."""
|
|
197
|
+
wb = CalamineWorkbook.from_path(HYPERLINK_TEST)
|
|
198
|
+
assert wb.get_sheet_hyperlink_formulas("Links", limit=1) == [
|
|
199
|
+
(0, 2, '=HYPERLINK("https://example.com/f","F")'),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
def test_limit_counts_only_hyperlink_formulas(self) -> None:
|
|
203
|
+
"""Catches a limit that counts the other formulas, which cuts a link after many plain formulas."""
|
|
204
|
+
wb = CalamineWorkbook.from_path(HYPERLINK_TEST)
|
|
205
|
+
assert wb.get_sheet_hyperlink_formulas("Links", limit=2) == [
|
|
206
|
+
(0, 2, '=HYPERLINK("https://example.com/f","F")'),
|
|
207
|
+
(2, 2, '=hyperlink("https://example.com/g")'),
|
|
208
|
+
]
|
|
209
|
+
assert wb.get_sheet_hyperlink_formulas("Links", limit=0) == []
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/python/python_calamine/__init__.py
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/python/python_calamine/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/create_richtext_fixture.py
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/create_test_fixture.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xlsb
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/dropdown_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_file.xlsb
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_file.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/empty_merge_test.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/formula_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/image_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/multi_image_test.xlsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/richtext_test.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/table-multiple.xlsb
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/table-multiple.xlsx
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/data/temperature-table.xlsb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_bounded_reads.py
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_gap_compression.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_trim_header_only.py
RENAMED
|
File without changes
|
{python_calamine_reducto-0.7.19 → python_calamine_reducto-0.7.20}/tests/test_uncached_formulas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|