Skip to content

Commit 3adf831

Browse files
committed
feat: add support for xlsx named table
1 parent a8f0695 commit 3adf831

File tree

13 files changed

+561
-110
lines changed

13 files changed

+561
-110
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
py
118118
${{ env.pythonLocation }}
119119
120-
- run: pip install pre-commit
120+
- run: pip install pre-commit mypy
121121
if: steps.cache-py.outputs.cache-hit != 'true'
122122

123123
- run: pip install .

.pre-commit-config.yaml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,20 @@ repos:
3434
rev: 24.1.1
3535
hooks:
3636
- id: black
37-
- repo: https://github.com/pre-commit/mirrors-mypy
38-
rev: v1.8.0
37+
- repo: local
3938
hooks:
4039
- id: mypy
41-
exclude: ^tests/.*$
40+
name: mypy
41+
entry: mypy
42+
language: python
43+
pass_filenames: false
44+
- id: mypy-stubtest
45+
name: mypy-stubtest
46+
entry: stubtest
47+
args:
48+
- python_calamine
49+
language: python
50+
pass_filenames: false
4251
- repo: local
4352
hooks:
4453
- id: rust-linting

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dynamic = ["version"]
2020
dev = [
2121
"maturin~=1.0",
2222
"pre-commit~=4.3",
23+
"mypy~=1.18.2",
2324
"pytest~=9.0",
2425
"pandas[excel]~=2.2",
2526
]
@@ -36,6 +37,7 @@ profile = "black"
3637

3738
[tool.mypy]
3839
python_version = "3.10"
40+
packages = ["python_calamine"]
3941
ignore_missing_imports = false
4042
disallow_untyped_defs = true
4143
check_untyped_defs = true

python/python_calamine/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from ._python_calamine import (
22
CalamineError,
33
CalamineSheet,
4+
CalamineTable,
45
CalamineWorkbook,
56
PasswordError,
67
SheetMetadata,
78
SheetTypeEnum,
89
SheetVisibleEnum,
10+
TableNotFound,
11+
TablesNotLoaded,
12+
TablesNotSupported,
913
WorkbookClosed,
1014
WorksheetNotFound,
1115
XmlError,
@@ -16,14 +20,18 @@
1620
__all__ = (
1721
"CalamineError",
1822
"CalamineSheet",
23+
"CalamineTable",
1924
"CalamineWorkbook",
2025
"PasswordError",
2126
"SheetMetadata",
2227
"SheetTypeEnum",
2328
"SheetVisibleEnum",
29+
"TableNotFound",
30+
"TablesNotLoaded",
31+
"TablesNotSupported",
32+
"WorkbookClosed",
2433
"WorksheetNotFound",
2534
"XmlError",
2635
"ZipError",
27-
"WorkbookClosed",
2836
"load_workbook",
2937
)

python/python_calamine/_python_calamine.pyi

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
# Some documentations from upstream under MIT License. See authors in https://github.com/tafia/calamine
12
from __future__ import annotations
23

3-
import contextlib
44
import datetime
55
import enum
66
import os
@@ -23,34 +23,57 @@ class SheetTypeEnum(enum.Enum):
2323
@typing.final
2424
class SheetVisibleEnum(enum.Enum):
2525
Visible = ...
26+
"""Visible."""
2627
Hidden = ...
28+
"""Hidden."""
2729
VeryHidden = ...
30+
"""The sheet is hidden and cannot be displayed using the user interface. It is supported only by Excel formats."""
2831

2932
@typing.final
3033
class SheetMetadata:
3134
name: str
35+
"""Name of sheet."""
3236
typ: SheetTypeEnum
37+
"""Type of sheet.
38+
39+
Only Excel formats support this. Default value for ODS is `WorkSheet`.
40+
"""
3341
visible: SheetVisibleEnum
42+
"""Visible of sheet."""
3443

35-
def __init__(
36-
self, name: str, typ: SheetTypeEnum, visible: SheetVisibleEnum
37-
) -> None: ...
44+
def __new__(
45+
cls, name: str, typ: SheetTypeEnum, visible: SheetVisibleEnum
46+
) -> SheetMetadata: ...
3847

3948
@typing.final
4049
class CalamineSheet:
4150
name: str
4251
@property
43-
def height(self) -> int: ...
52+
def height(self) -> int:
53+
"""Get the row height of a sheet data.
54+
55+
The height is defined as the number of rows between the start and end positions.
56+
"""
57+
4458
@property
45-
def width(self) -> int: ...
59+
def width(self) -> int:
60+
"""Get the column width of a sheet data.
61+
62+
The width is defined as the number of columns between the start and end positions.
63+
"""
64+
4665
@property
4766
def total_height(self) -> int: ...
4867
@property
4968
def total_width(self) -> int: ...
5069
@property
51-
def start(self) -> tuple[int, int] | None: ...
70+
def start(self) -> tuple[int, int] | None:
71+
"""Get top left cell position of a sheet data."""
72+
5273
@property
53-
def end(self) -> tuple[int, int] | None: ...
74+
def end(self) -> tuple[int, int] | None:
75+
"""Get bottom right cell position of a sheet data."""
76+
5477
def to_python(
5578
self, skip_empty_area: bool = True, nrows: int | None = None
5679
) -> list[
@@ -102,34 +125,96 @@ class CalamineSheet:
102125
"""
103126

104127
@typing.final
105-
class CalamineWorkbook(contextlib.AbstractContextManager):
128+
class CalamineTable:
129+
name: str
130+
"""Get the name of the table."""
131+
sheet: str
132+
"""Get the name of the parent worksheet for a table."""
133+
columns: list[str]
134+
"""Get the header names of the table columns.
135+
136+
In Excel table headers can be hidden but the table will still have
137+
column header names.
138+
"""
139+
@property
140+
def height(self) -> int:
141+
"""Get the row height of a table data.
142+
143+
The height is defined as the number of rows between the start and end positions.
144+
"""
145+
146+
@property
147+
def width(self) -> int:
148+
"""Get the column width of a table data.
149+
150+
The width is defined as the number of columns between the start and end positions.
151+
"""
152+
153+
@property
154+
def start(self) -> tuple[int, int] | None:
155+
"""Get top left cell position of a table data."""
156+
157+
@property
158+
def end(self) -> tuple[int, int] | None:
159+
"""Get bottom right cell position of a table data."""
160+
161+
def to_python(
162+
self,
163+
) -> list[
164+
list[
165+
int
166+
| float
167+
| str
168+
| bool
169+
| datetime.time
170+
| datetime.date
171+
| datetime.datetime
172+
| datetime.timedelta
173+
]
174+
]:
175+
"""Retunrning data from table as list of lists."""
176+
177+
@typing.final
178+
class CalamineWorkbook:
106179
path: str | None
180+
"""Path to file. `None` if bytes was loaded."""
107181
sheet_names: list[str]
182+
"""All sheet names of this workbook, in workbook order."""
108183
sheets_metadata: list[SheetMetadata]
184+
"""All sheets metadata of this workbook, in workbook order."""
185+
table_names: list[str] | None
186+
"""All table names of this workbook."""
109187
@classmethod
110188
def from_object(
111-
cls, path_or_filelike: str | os.PathLike | ReadBuffer
189+
cls, path_or_filelike: str | os.PathLike | ReadBuffer, load_tables: bool = False
112190
) -> "CalamineWorkbook":
113191
"""Determining type of pyobject and reading from it.
114192
115193
Args:
116194
path_or_filelike (str | os.PathLike | ReadBuffer): path to file or IO (must imlpement read/seek methods).
195+
load_tables (bool): load Excel tables (supported for XLSX only).
117196
"""
118197

119198
@classmethod
120-
def from_path(cls, path: str | os.PathLike) -> "CalamineWorkbook":
199+
def from_path(
200+
cls, path: str | os.PathLike, load_tables: bool = False
201+
) -> "CalamineWorkbook":
121202
"""Reading file from path.
122203
123204
Args:
124205
path (str | os.PathLike): path to file.
206+
load_tables (bool): load Excel tables (supported for XLSX only).
125207
"""
126208

127209
@classmethod
128-
def from_filelike(cls, filelike: ReadBuffer) -> "CalamineWorkbook":
210+
def from_filelike(
211+
cls, filelike: ReadBuffer, load_tables: bool = False
212+
) -> "CalamineWorkbook":
129213
"""Reading file from IO.
130214
131215
Args:
132216
filelike : IO (must imlpement read/seek methods).
217+
load_tables (bool): load Excel tables (supported for XLSX only).
133218
"""
134219

135220
def close(self) -> None:
@@ -177,18 +262,55 @@ class CalamineWorkbook(contextlib.AbstractContextManager):
177262
WorksheetNotFound: If worksheet not found in workbook.
178263
"""
179264

265+
def get_table_by_name(self, name: str) -> CalamineTable:
266+
"""Get table by name.
267+
268+
Args:
269+
name(str): name of table
270+
271+
Returns:
272+
CalamineTable
273+
274+
Raises:
275+
WorkbookClosed: If workbook already closed.
276+
WorksheetNotFound: If worksheet not found in workbook.
277+
"""
278+
180279
class CalamineError(Exception): ...
181280
class PasswordError(CalamineError): ...
182281
class WorksheetNotFound(CalamineError): ...
183282
class XmlError(CalamineError): ...
184283
class ZipError(CalamineError): ...
185284
class WorkbookClosed(CalamineError): ...
285+
class TablesNotLoaded(CalamineError): ...
286+
class TablesNotSupported(CalamineError): ...
287+
class TableNotFound(CalamineError): ...
186288

187289
def load_workbook(
188-
path_or_filelike: str | os.PathLike | ReadBuffer,
290+
path_or_filelike: str | os.PathLike | ReadBuffer, load_tables: bool = False
189291
) -> CalamineWorkbook:
190292
"""Determining type of pyobject and reading from it.
191293
192294
Args:
193295
path_or_filelike (str | os.PathLike | ReadBuffer): path to file or IO (must imlpement read/seek methods).
296+
load_tables (bool): load Excel tables (supported for XLSX only).
194297
"""
298+
299+
__all__ = [
300+
"CalamineError",
301+
"CalamineSheet",
302+
"CalamineTable",
303+
"CalamineWorkbook",
304+
"PasswordError",
305+
"SheetMetadata",
306+
"SheetTypeEnum",
307+
"SheetVisibleEnum",
308+
"TableNotFound",
309+
"TablesNotLoaded",
310+
"TablesNotSupported",
311+
"WorkbookClosed",
312+
"WorksheetNotFound",
313+
"XmlError",
314+
"ZipError",
315+
"load_workbook",
316+
]

src/lib.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
use pyo3::prelude::*;
22

33
mod types;
4-
mod utils;
54
use crate::types::{
6-
CalamineError, CalamineSheet, CalamineWorkbook, CellValue, Error, PasswordError, SheetMetadata,
7-
SheetTypeEnum, SheetVisibleEnum, WorkbookClosed, WorksheetNotFound, XmlError, ZipError,
5+
CalamineError, CalamineSheet, CalamineTable, CalamineWorkbook, CellValue, Error, PasswordError,
6+
SheetMetadata, SheetTypeEnum, SheetVisibleEnum, TableNotFound, TablesNotLoaded,
7+
TablesNotSupported, WorkbookClosed, WorksheetNotFound, XmlError, ZipError,
88
};
99

1010
#[pyfunction]
11-
fn load_workbook(py: Python, path_or_filelike: Py<PyAny>) -> PyResult<CalamineWorkbook> {
12-
CalamineWorkbook::from_object(py, path_or_filelike)
11+
#[pyo3(signature = (path_or_filelike, load_tables=false))]
12+
fn load_workbook(
13+
py: Python,
14+
path_or_filelike: Py<PyAny>,
15+
load_tables: bool,
16+
) -> PyResult<CalamineWorkbook> {
17+
CalamineWorkbook::from_object(py, path_or_filelike, load_tables)
1318
}
1419

1520
#[pymodule(gil_used = false)]
@@ -20,11 +25,15 @@ fn _python_calamine(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
2025
m.add_class::<SheetMetadata>()?;
2126
m.add_class::<SheetTypeEnum>()?;
2227
m.add_class::<SheetVisibleEnum>()?;
28+
m.add_class::<CalamineTable>()?;
2329
m.add("CalamineError", py.get_type::<CalamineError>())?;
2430
m.add("PasswordError", py.get_type::<PasswordError>())?;
2531
m.add("WorksheetNotFound", py.get_type::<WorksheetNotFound>())?;
2632
m.add("XmlError", py.get_type::<XmlError>())?;
2733
m.add("ZipError", py.get_type::<ZipError>())?;
34+
m.add("TablesNotSupported", py.get_type::<TablesNotSupported>())?;
35+
m.add("TablesNotLoaded", py.get_type::<TablesNotLoaded>())?;
36+
m.add("TableNotFound", py.get_type::<TableNotFound>())?;
2837
m.add("WorkbookClosed", py.get_type::<WorkbookClosed>())?;
2938
Ok(())
3039
}

0 commit comments

Comments
 (0)