python-calamine 0.3.2__tar.gz → 0.5.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 (48) hide show
  1. {python_calamine-0.3.2 → python_calamine-0.5.0}/.github/workflows/CI.yml +26 -39
  2. {python_calamine-0.3.2 → python_calamine-0.5.0}/.pre-commit-config.yaml +0 -2
  3. {python_calamine-0.3.2 → python_calamine-0.5.0}/Cargo.lock +42 -68
  4. python_calamine-0.5.0/Cargo.toml +22 -0
  5. {python_calamine-0.3.2 → python_calamine-0.5.0}/PKG-INFO +6 -26
  6. {python_calamine-0.3.2 → python_calamine-0.5.0}/README.md +0 -13
  7. {python_calamine-0.3.2 → python_calamine-0.5.0}/pyproject.toml +9 -9
  8. {python_calamine-0.3.2 → python_calamine-0.5.0}/python/python_calamine/_python_calamine.pyi +12 -0
  9. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/lib.rs +1 -1
  10. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/types/cell.rs +32 -4
  11. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/types/sheet.rs +19 -4
  12. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/types/workbook.rs +56 -20
  13. python_calamine-0.5.0/tests/data/base.ods +0 -0
  14. python_calamine-0.5.0/tests/data/base.xls +0 -0
  15. python_calamine-0.5.0/tests/data/base.xlsb +0 -0
  16. python_calamine-0.5.0/tests/data/base.xlsx +0 -0
  17. python_calamine-0.5.0/tests/data/issue139.xlsx +0 -0
  18. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/test_base.py +24 -5
  19. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/test_pandas.py +0 -13
  20. python_calamine-0.3.2/Cargo.toml +0 -27
  21. python_calamine-0.3.2/python/python_calamine/pandas.py +0 -136
  22. python_calamine-0.3.2/tests/data/base.ods +0 -0
  23. python_calamine-0.3.2/tests/data/base.xls +0 -0
  24. python_calamine-0.3.2/tests/data/base.xlsb +0 -0
  25. python_calamine-0.3.2/tests/data/base.xlsx +0 -0
  26. {python_calamine-0.3.2 → python_calamine-0.5.0}/.gitignore +0 -0
  27. {python_calamine-0.3.2 → python_calamine-0.5.0}/LICENSE +0 -0
  28. {python_calamine-0.3.2 → python_calamine-0.5.0}/build.rs +0 -0
  29. {python_calamine-0.3.2 → python_calamine-0.5.0}/python/python_calamine/__init__.py +0 -0
  30. {python_calamine-0.3.2 → python_calamine-0.5.0}/python/python_calamine/py.typed +0 -0
  31. {python_calamine-0.3.2 → python_calamine-0.5.0}/renovate.json +0 -0
  32. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/types/errors.rs +0 -0
  33. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/types/mod.rs +0 -0
  34. {python_calamine-0.3.2 → python_calamine-0.5.0}/src/utils.rs +0 -0
  35. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/__init__.py +0 -0
  36. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/conftest.py +0 -0
  37. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/any_sheets.ods +0 -0
  38. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/any_sheets.xls +0 -0
  39. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/any_sheets.xlsb +0 -0
  40. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/any_sheets.xlsx +0 -0
  41. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/empty_file.ods +0 -0
  42. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/empty_file.xlsb +0 -0
  43. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/empty_file.xlsx +0 -0
  44. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/password.ods +0 -0
  45. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/password.xls +0 -0
  46. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/password.xlsb +0 -0
  47. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/data/password.xlsx +0 -0
  48. {python_calamine-0.3.2 → python_calamine-0.5.0}/tests/test_sheet_type.py +0 -0
@@ -13,33 +13,29 @@ on:
13
13
 
14
14
  jobs:
15
15
  test:
16
- name: test ${{ matrix.python-version }} pandas ${{ matrix.pandas-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
16
+ name: test ${{ matrix.python-version }} pandas ${{ matrix.pandas-version }}
17
17
  strategy:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  python-version:
21
- - "3.9"
22
21
  - "3.10"
23
22
  - "3.11"
24
23
  - "3.12"
24
+ - "3.13"
25
+ - "3.14"
25
26
  pandas-version:
26
- - "2.1.*"
27
27
  - "2.2.*"
28
+ - "2.3.*"
28
29
  include:
29
- - python-version: "3.8"
30
- pandas-version: "none"
31
30
  # https://github.com/pandas-dev/pandas/issues/42509
32
- - python-version: "pypy3.9"
31
+ - python-version: "pypy3.11"
33
32
  pandas-version: "none"
34
- # https://github.com/pandas-dev/pandas/issues/42509
35
- - python-version: "pypy3.10"
33
+ # https://github.com/pandas-dev/pandas/issues/60016
34
+ - python-version: "3.13t"
36
35
  pandas-version: "none"
37
- - python-version: "3.13"
38
- pandas-version: "2.2.*"
39
36
  # https://github.com/pandas-dev/pandas/issues/60016
40
- - python-version: "3.13"
37
+ - python-version: "3.14t"
41
38
  pandas-version: "none"
42
- disable-gil: true
43
39
 
44
40
  runs-on: ubuntu-latest
45
41
 
@@ -47,7 +43,7 @@ jobs:
47
43
  PYTHON: ${{ matrix.python-version }}
48
44
 
49
45
  steps:
50
- - uses: actions/checkout@v4
46
+ - uses: actions/checkout@v5
51
47
 
52
48
  - name: install rust stable
53
49
  uses: dtolnay/rust-toolchain@master
@@ -61,29 +57,22 @@ jobs:
61
57
  key: v3
62
58
 
63
59
  - name: set up python
64
- uses: actions/setup-python@v5
60
+ uses: actions/setup-python@v6
65
61
  if: "${{ !matrix.disable-gil }}"
66
62
  with:
67
63
  python-version: ${{ matrix.python-version }}
68
64
  allow-prereleases: true
69
65
 
70
- - name: set up python (free-threaded)
71
- uses: deadsnakes/action@v3.2.0
72
- if: "${{ matrix.disable-gil }}"
73
- with:
74
- python-version: ${{ matrix.python-version }}
75
- nogil: ${{ matrix.disable-gil }}
76
-
77
66
  - id: cache-py
78
67
  name: cache python
79
68
  uses: actions/cache@v4
80
- if: "${{ !matrix.disable-gil }}"
81
69
  with:
82
70
  path: ${{ env.pythonLocation }}
83
71
  key: >
84
72
  py
85
73
  ${{ runner.os }}
86
- ${{ env.pythonLocation }}
74
+ ${{ matrix.python-version }}
75
+ ${{ matrix.pandas-version }}
87
76
 
88
77
  - run: |
89
78
  if [ ${{ matrix.pandas-version }} == 'none' ]; then
@@ -105,7 +94,7 @@ jobs:
105
94
  runs-on: ubuntu-latest
106
95
 
107
96
  steps:
108
- - uses: actions/checkout@v4
97
+ - uses: actions/checkout@v5
109
98
 
110
99
  - name: install rust stable
111
100
  uses: dtolnay/rust-toolchain@stable
@@ -115,7 +104,7 @@ jobs:
115
104
  - name: cache rust
116
105
  uses: Swatinem/rust-cache@v2
117
106
 
118
- - uses: actions/setup-python@v5
107
+ - uses: actions/setup-python@v6
119
108
  with:
120
109
  python-version: "3.13"
121
110
 
@@ -155,7 +144,7 @@ jobs:
155
144
  strategy:
156
145
  fail-fast: false
157
146
  matrix:
158
- os: [ubuntu, macos, windows]
147
+ os: [ubuntu, macos]
159
148
  target: [x86_64, aarch64]
160
149
  manylinux: [auto]
161
150
  include:
@@ -163,15 +152,17 @@ jobs:
163
152
  platform: linux
164
153
  - os: windows
165
154
  ls: dir
155
+ target: x86_64
156
+ interpreter: 3.10 3.11 3.12 3.13 3.13t
166
157
  - os: windows
167
158
  ls: dir
168
159
  target: i686
169
160
  python-architecture: x86
170
- interpreter: 3.8 3.9 3.10 3.11 3.12 3.13
161
+ interpreter: 3.10 3.11 3.12 3.13 3.13t
171
162
  - os: windows
172
163
  ls: dir
173
164
  target: aarch64
174
- interpreter: 3.11 3.12 3.13
165
+ interpreter: 3.11 3.12 3.13 3.13t
175
166
  - os: ubuntu
176
167
  platform: linux
177
168
  target: i686
@@ -181,7 +172,7 @@ jobs:
181
172
  - os: ubuntu
182
173
  platform: linux
183
174
  target: armv7
184
- interpreter: 3.8 3.9 3.10 3.11 3.12 3.13
175
+ interpreter: 3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.11
185
176
  # musllinux
186
177
  - os: ubuntu
187
178
  platform: linux
@@ -194,22 +185,18 @@ jobs:
194
185
  - os: ubuntu
195
186
  platform: linux
196
187
  target: ppc64le
197
- interpreter: 3.8 3.9 3.10 3.11 3.12 3.13
188
+ interpreter: 3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.11
198
189
  - os: ubuntu
199
190
  platform: linux
200
191
  target: s390x
201
- interpreter: 3.8 3.9 3.10 3.11 3.12 3.13
202
- exclude:
203
- # Windows on arm64 only supports Python 3.11+
204
- - os: windows
205
- target: aarch64
192
+ interpreter: 3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.11
206
193
 
207
194
  runs-on: ${{ matrix.os }}-latest
208
195
  steps:
209
- - uses: actions/checkout@v4
196
+ - uses: actions/checkout@v5
210
197
 
211
198
  - name: set up python
212
- uses: actions/setup-python@v5
199
+ uses: actions/setup-python@v6
213
200
  with:
214
201
  python-version: "3.13"
215
202
  architecture: ${{ matrix.python-architecture || 'x64' }}
@@ -229,7 +216,7 @@ jobs:
229
216
  with:
230
217
  target: ${{ matrix.target }}
231
218
  manylinux: ${{ matrix.manylinux || 'auto' }}
232
- args: --release --out dist --interpreter ${{ matrix.interpreter || '3.8 3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10' }}
219
+ args: --release --out dist --interpreter ${{ matrix.interpreter || '3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.11' }}
233
220
  rust-toolchain: stable
234
221
 
235
222
  - run: ${{ matrix.ls || 'ls -lh' }} dist/
@@ -256,7 +243,7 @@ jobs:
256
243
 
257
244
  steps:
258
245
  - name: Download All Artifacts
259
- uses: actions/download-artifact@v4
246
+ uses: actions/download-artifact@v5
260
247
  with:
261
248
  path: dist
262
249
  pattern: wheels-*
@@ -38,8 +38,6 @@ repos:
38
38
  rev: v1.8.0
39
39
  hooks:
40
40
  - id: mypy
41
- additional_dependencies:
42
- - pandas-stubs
43
41
  exclude: ^tests/.*$
44
42
  - repo: local
45
43
  hooks:
@@ -70,8 +70,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
70
70
 
71
71
  [[package]]
72
72
  name = "calamine"
73
- version = "0.26.1"
74
- source = "git+https://github.com/tafia/calamine.git?rev=5cda5c734661f656bdfb7ac7a1b95e77bf89aa09#5cda5c734661f656bdfb7ac7a1b95e77bf89aa09"
73
+ version = "0.30.0"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "a9aeb09f84576a634da713630e11e431a744b91f1f8114c2ff0760189783a8a1"
75
76
  dependencies = [
76
77
  "atoi_simd",
77
78
  "byteorder",
@@ -133,9 +134,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
133
134
 
134
135
  [[package]]
135
136
  name = "chrono"
136
- version = "0.4.40"
137
+ version = "0.4.41"
137
138
  source = "registry+https://github.com/rust-lang/crates.io-index"
138
- checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
139
+ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
139
140
  dependencies = [
140
141
  "android-tzdata",
141
142
  "iana-time-zone",
@@ -170,12 +171,6 @@ dependencies = [
170
171
  "cfg-if",
171
172
  ]
172
173
 
173
- [[package]]
174
- name = "crossbeam-utils"
175
- version = "0.8.21"
176
- source = "registry+https://github.com/rust-lang/crates.io-index"
177
- checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
178
-
179
174
  [[package]]
180
175
  name = "derive_arbitrary"
181
176
  version = "1.4.1"
@@ -187,17 +182,6 @@ dependencies = [
187
182
  "syn",
188
183
  ]
189
184
 
190
- [[package]]
191
- name = "displaydoc"
192
- version = "0.2.5"
193
- source = "registry+https://github.com/rust-lang/crates.io-index"
194
- checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
195
- dependencies = [
196
- "proc-macro2",
197
- "quote",
198
- "syn",
199
- ]
200
-
201
185
  [[package]]
202
186
  name = "encoding_rs"
203
187
  version = "0.8.35"
@@ -246,11 +230,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
246
230
 
247
231
  [[package]]
248
232
  name = "flate2"
249
- version = "1.0.35"
233
+ version = "1.1.2"
250
234
  source = "registry+https://github.com/rust-lang/crates.io-index"
251
- checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
235
+ checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
252
236
  dependencies = [
253
237
  "crc32fast",
238
+ "libz-rs-sys",
254
239
  "miniz_oxide",
255
240
  ]
256
241
 
@@ -344,6 +329,15 @@ version = "0.2.169"
344
329
  source = "registry+https://github.com/rust-lang/crates.io-index"
345
330
  checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
346
331
 
332
+ [[package]]
333
+ name = "libz-rs-sys"
334
+ version = "0.5.1"
335
+ source = "registry+https://github.com/rust-lang/crates.io-index"
336
+ checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
337
+ dependencies = [
338
+ "zlib-rs",
339
+ ]
340
+
347
341
  [[package]]
348
342
  name = "linux-raw-sys"
349
343
  version = "0.4.15"
@@ -379,9 +373,9 @@ dependencies = [
379
373
 
380
374
  [[package]]
381
375
  name = "miniz_oxide"
382
- version = "0.8.2"
376
+ version = "0.8.9"
383
377
  source = "registry+https://github.com/rust-lang/crates.io-index"
384
- checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
378
+ checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
385
379
  dependencies = [
386
380
  "adler2",
387
381
  ]
@@ -397,9 +391,9 @@ dependencies = [
397
391
 
398
392
  [[package]]
399
393
  name = "once_cell"
400
- version = "1.20.2"
394
+ version = "1.21.3"
401
395
  source = "registry+https://github.com/rust-lang/crates.io-index"
402
- checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
396
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
403
397
 
404
398
  [[package]]
405
399
  name = "portable-atomic"
@@ -429,11 +423,10 @@ dependencies = [
429
423
 
430
424
  [[package]]
431
425
  name = "pyo3"
432
- version = "0.24.1"
426
+ version = "0.26.0"
433
427
  source = "registry+https://github.com/rust-lang/crates.io-index"
434
- checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
428
+ checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383"
435
429
  dependencies = [
436
- "cfg-if",
437
430
  "chrono",
438
431
  "indoc",
439
432
  "libc",
@@ -448,20 +441,19 @@ dependencies = [
448
441
 
449
442
  [[package]]
450
443
  name = "pyo3-build-config"
451
- version = "0.24.1"
444
+ version = "0.26.0"
452
445
  source = "registry+https://github.com/rust-lang/crates.io-index"
453
- checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
446
+ checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f"
454
447
  dependencies = [
455
- "once_cell",
456
448
  "python3-dll-a",
457
449
  "target-lexicon",
458
450
  ]
459
451
 
460
452
  [[package]]
461
453
  name = "pyo3-ffi"
462
- version = "0.24.1"
454
+ version = "0.26.0"
463
455
  source = "registry+https://github.com/rust-lang/crates.io-index"
464
- checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
456
+ checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105"
465
457
  dependencies = [
466
458
  "libc",
467
459
  "pyo3-build-config",
@@ -469,9 +461,8 @@ dependencies = [
469
461
 
470
462
  [[package]]
471
463
  name = "pyo3-file"
472
- version = "0.12.0"
473
- source = "registry+https://github.com/rust-lang/crates.io-index"
474
- checksum = "8d97fbb05588c29774af01833e598fae7e714a3f87d36b286e0466f6b678e4a7"
464
+ version = "0.13.0"
465
+ source = "git+https://github.com/dimastbk/pyo3-file.git?rev=da8ddb5d774d042555b867dd6571f3ce128bc349#da8ddb5d774d042555b867dd6571f3ce128bc349"
475
466
  dependencies = [
476
467
  "pyo3",
477
468
  "skeptic",
@@ -479,9 +470,9 @@ dependencies = [
479
470
 
480
471
  [[package]]
481
472
  name = "pyo3-macros"
482
- version = "0.24.1"
473
+ version = "0.26.0"
483
474
  source = "registry+https://github.com/rust-lang/crates.io-index"
484
- checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
475
+ checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded"
485
476
  dependencies = [
486
477
  "proc-macro2",
487
478
  "pyo3-macros-backend",
@@ -491,9 +482,9 @@ dependencies = [
491
482
 
492
483
  [[package]]
493
484
  name = "pyo3-macros-backend"
494
- version = "0.24.1"
485
+ version = "0.26.0"
495
486
  source = "registry+https://github.com/rust-lang/crates.io-index"
496
- checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
487
+ checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf"
497
488
  dependencies = [
498
489
  "heck",
499
490
  "proc-macro2",
@@ -504,7 +495,7 @@ dependencies = [
504
495
 
505
496
  [[package]]
506
497
  name = "python-calamine"
507
- version = "0.3.2"
498
+ version = "0.5.0"
508
499
  dependencies = [
509
500
  "calamine",
510
501
  "chrono",
@@ -668,26 +659,6 @@ dependencies = [
668
659
  "windows-sys",
669
660
  ]
670
661
 
671
- [[package]]
672
- name = "thiserror"
673
- version = "2.0.10"
674
- source = "registry+https://github.com/rust-lang/crates.io-index"
675
- checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3"
676
- dependencies = [
677
- "thiserror-impl",
678
- ]
679
-
680
- [[package]]
681
- name = "thiserror-impl"
682
- version = "2.0.10"
683
- source = "registry+https://github.com/rust-lang/crates.io-index"
684
- checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb"
685
- dependencies = [
686
- "proc-macro2",
687
- "quote",
688
- "syn",
689
- ]
690
-
691
662
  [[package]]
692
663
  name = "unicase"
693
664
  version = "2.8.1"
@@ -881,21 +852,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
881
852
 
882
853
  [[package]]
883
854
  name = "zip"
884
- version = "2.4.1"
855
+ version = "4.5.0"
885
856
  source = "registry+https://github.com/rust-lang/crates.io-index"
886
- checksum = "938cc23ac49778ac8340e366ddc422b2227ea176edb447e23fc0627608dddadd"
857
+ checksum = "8835eb39822904d39cb19465de1159e05d371973f0c6df3a365ad50565ddc8b9"
887
858
  dependencies = [
888
859
  "arbitrary",
889
860
  "crc32fast",
890
- "crossbeam-utils",
891
- "displaydoc",
892
861
  "flate2",
893
862
  "indexmap",
894
863
  "memchr",
895
- "thiserror",
896
864
  "zopfli",
897
865
  ]
898
866
 
867
+ [[package]]
868
+ name = "zlib-rs"
869
+ version = "0.5.1"
870
+ source = "registry+https://github.com/rust-lang/crates.io-index"
871
+ checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
872
+
899
873
  [[package]]
900
874
  name = "zopfli"
901
875
  version = "0.8.1"
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "python-calamine"
3
+ version = "0.5.0"
4
+ edition = "2021"
5
+ readme = "README.md"
6
+
7
+ [lib]
8
+ name = "python_calamine"
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ calamine = { version = "0.30.0", features = ["dates"] }
13
+ pyo3 = { version = "0.26.0", features = [
14
+ "extension-module",
15
+ "chrono",
16
+ "generate-import-lib",
17
+ ] }
18
+ chrono = { version = "0.4.38", features = ["serde"] }
19
+ pyo3-file = { git = "https://github.com/dimastbk/pyo3-file.git", rev = "da8ddb5d774d042555b867dd6571f3ce128bc349" }
20
+
21
+ [build-dependencies]
22
+ pyo3-build-config = "0.26.0"
@@ -1,31 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-calamine
3
- Version: 0.3.2
3
+ Version: 0.5.0
4
4
  Classifier: Development Status :: 4 - Beta
5
- Classifier: License :: OSI Approved :: MIT License
6
5
  Classifier: Programming Language :: Rust
7
- Classifier: Programming Language :: Python :: 3.8
8
- Classifier: Programming Language :: Python :: 3.9
9
6
  Classifier: Programming Language :: Python :: 3.10
10
7
  Classifier: Programming Language :: Python :: 3.11
11
8
  Classifier: Programming Language :: Python :: 3.12
12
9
  Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: packaging>=23.1
10
+ Classifier: Programming Language :: Python :: 3.14
14
11
  Requires-Dist: maturin~=1.0 ; extra == 'dev'
15
- Requires-Dist: pre-commit~=3.0 ; extra == 'dev'
12
+ Requires-Dist: pre-commit~=4.3 ; extra == 'dev'
16
13
  Requires-Dist: pytest~=8.0 ; extra == 'dev'
17
- Requires-Dist: pandas[excel]~=2.0 ; extra == 'dev'
18
- Requires-Dist: numpy~=1.0 ; extra == 'dev'
14
+ Requires-Dist: pandas[excel]~=2.2 ; extra == 'dev'
19
15
  Provides-Extra: dev
20
16
  License-File: LICENSE
21
17
  Summary: Python binding for Rust's library for reading excel and odf file - calamine
22
- Home-Page: https://github.com/dimastbk/python-calamine
23
- Author: Dmitriy <dimastbk@proton.me>
24
18
  Author-email: Dmitriy <dimastbk@proton.me>
25
- License: MIT
26
- Requires-Python: >=3.8
19
+ License-Expression: MIT
20
+ Requires-Python: >=3.10
27
21
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
28
- Project-URL: Source Code, https://github.com/dimastbk/python-calamine
29
22
 
30
23
  # python-calamine
31
24
  [![PyPI - Version](https://img.shields.io/pypi/v/python-calamine)](https://pypi.org/project/python-calamine/)
@@ -79,20 +72,7 @@ workbook = CalamineWorkbook.from_path("file.xlsx").get_sheet_by_name("Sheet1").t
79
72
  # ]
80
73
  ```
81
74
 
82
- Also, you can use monkeypatch for pandas for use this library as engine in `read_excel()` (only pandas 2.0 and 2.1 are supported).
83
75
  Pandas 2.2 and above have built-in support of python-calamine.
84
- ```python
85
- from pandas import read_excel
86
- from python_calamine.pandas import pandas_monkeypatch
87
-
88
-
89
- pandas_monkeypatch()
90
-
91
- read_excel("file.xlsx", engine="calamine")
92
- # 1 2 3 4 5 6 7
93
- # 0 1 2 3 4 5 6 7
94
- # 1 1 2 3 4 5 6 7
95
- ```
96
76
 
97
77
  Also, you can find additional examples in [tests](https://github.com/dimastbk/python-calamine/blob/master/tests/test_base.py).
98
78
 
@@ -50,20 +50,7 @@ workbook = CalamineWorkbook.from_path("file.xlsx").get_sheet_by_name("Sheet1").t
50
50
  # ]
51
51
  ```
52
52
 
53
- Also, you can use monkeypatch for pandas for use this library as engine in `read_excel()` (only pandas 2.0 and 2.1 are supported).
54
53
  Pandas 2.2 and above have built-in support of python-calamine.
55
- ```python
56
- from pandas import read_excel
57
- from python_calamine.pandas import pandas_monkeypatch
58
-
59
-
60
- pandas_monkeypatch()
61
-
62
- read_excel("file.xlsx", engine="calamine")
63
- # 1 2 3 4 5 6 7
64
- # 0 1 2 3 4 5 6 7
65
- # 1 1 2 3 4 5 6 7
66
- ```
67
54
 
68
55
  Also, you can find additional examples in [tests](https://github.com/dimastbk/python-calamine/blob/master/tests/test_base.py).
69
56
 
@@ -1,27 +1,27 @@
1
1
  [project]
2
2
  name = "python-calamine"
3
- requires-python = ">=3.8"
3
+ description = "Python binding for Rust's library for reading excel and odf file - calamine"
4
+ readme = "README.md"
5
+ requires-python = ">=3.10"
6
+ license = "MIT"
7
+ authors = [{ name = "Dmitriy", email = "dimastbk@proton.me" }]
4
8
  classifiers = [
5
9
  "Development Status :: 4 - Beta",
6
- "License :: OSI Approved :: MIT License",
7
10
  "Programming Language :: Rust",
8
- "Programming Language :: Python :: 3.8",
9
- "Programming Language :: Python :: 3.9",
10
11
  "Programming Language :: Python :: 3.10",
11
12
  "Programming Language :: Python :: 3.11",
12
13
  "Programming Language :: Python :: 3.12",
13
14
  "Programming Language :: Python :: 3.13",
15
+ "Programming Language :: Python :: 3.14",
14
16
  ]
15
- dependencies = ["packaging>=23.1"]
16
17
  dynamic = ["version"]
17
18
 
18
19
  [project.optional-dependencies]
19
20
  dev = [
20
21
  "maturin~=1.0",
21
- "pre-commit~=3.0",
22
+ "pre-commit~=4.3",
22
23
  "pytest~=8.0",
23
- "pandas[excel]~=2.0",
24
- "numpy~=1.0",
24
+ "pandas[excel]~=2.2",
25
25
  ]
26
26
 
27
27
  [tool.isort]
@@ -31,7 +31,7 @@ multi_line_output = 3
31
31
  profile = "black"
32
32
 
33
33
  [tool.mypy]
34
- python_version = "3.8"
34
+ python_version = "3.10"
35
35
  ignore_missing_imports = false
36
36
  disallow_untyped_defs = true
37
37
  check_untyped_defs = true
@@ -89,6 +89,18 @@ class CalamineSheet:
89
89
  ]:
90
90
  """Retunrning data from sheet as iterator of lists."""
91
91
 
92
+ @property
93
+ def merged_cell_ranges(
94
+ self,
95
+ ) -> list[tuple[tuple[int, int], tuple[int, int]]] | None:
96
+ """Return a copy of merged cell ranges.
97
+
98
+ Support only for xlsx/xls.
99
+
100
+ Returns:
101
+ list of merged cell ranges (tuple[start coordinate, end coordinate]) or None for unsuported format
102
+ """
103
+
92
104
  @typing.final
93
105
  class CalamineWorkbook(contextlib.AbstractContextManager):
94
106
  path: str | None
@@ -8,7 +8,7 @@ use crate::types::{
8
8
  };
9
9
 
10
10
  #[pyfunction]
11
- fn load_workbook(py: Python, path_or_filelike: PyObject) -> PyResult<CalamineWorkbook> {
11
+ fn load_workbook(py: Python, path_or_filelike: Py<PyAny>) -> PyResult<CalamineWorkbook> {
12
12
  CalamineWorkbook::from_object(py, path_or_filelike)
13
13
  }
14
14
 
@@ -1,8 +1,16 @@
1
1
  use std::convert::From;
2
2
 
3
3
  use calamine::DataType;
4
+ use chrono::Datelike;
4
5
  use pyo3::prelude::*;
5
6
 
7
+ /// https://docs.python.org/3/library/datetime.html#constants
8
+ /// The smallest year number allowed in a date or datetime object. MINYEAR is 1.
9
+ const MINYEAR: i32 = 1;
10
+
11
+ /// The largest year number allowed in a date or datetime object. MAXYEAR is 9999.
12
+ const MAXYEAR: i32 = 9999;
13
+
6
14
  #[derive(Debug, Clone)]
7
15
  pub enum CellValue {
8
16
  Int(i64),
@@ -16,6 +24,14 @@ pub enum CellValue {
16
24
  Empty,
17
25
  }
18
26
 
27
+ fn check_year_range<DT: Datelike>(value: DT) -> Option<DT> {
28
+ if value.year() < MINYEAR || value.year() > MAXYEAR {
29
+ None
30
+ } else {
31
+ Some(value)
32
+ }
33
+ }
34
+
19
35
  impl<'py> IntoPyObject<'py> for CellValue {
20
36
  type Target = PyAny;
21
37
  type Output = Bound<'py, Self::Target>;
@@ -64,19 +80,31 @@ where
64
80
  } else if v < 1.0 {
65
81
  value.as_time().map(CellValue::Time)
66
82
  } else if v == (v as u64) as f64 {
67
- value.as_date().map(CellValue::Date)
83
+ value
84
+ .as_date()
85
+ .and_then(check_year_range)
86
+ .map(CellValue::Date)
68
87
  } else {
69
- value.as_datetime().map(CellValue::DateTime)
88
+ value
89
+ .as_datetime()
90
+ .and_then(check_year_range)
91
+ .map(CellValue::DateTime)
70
92
  }
71
93
  .unwrap_or(CellValue::Float(v))
72
94
  } else if value.is_datetime_iso() {
73
95
  let v = value.get_datetime_iso().unwrap();
74
96
  if v.contains('T') {
75
- value.as_datetime().map(CellValue::DateTime)
97
+ value
98
+ .as_datetime()
99
+ .and_then(check_year_range)
100
+ .map(CellValue::DateTime)
76
101
  } else if v.contains(':') {
77
102
  value.as_time().map(CellValue::Time)
78
103
  } else {
79
- value.as_date().map(CellValue::Date)
104
+ value
105
+ .as_date()
106
+ .and_then(check_year_range)
107
+ .map(CellValue::Date)
80
108
  }
81
109
  .unwrap_or(CellValue::String(v.to_owned()))
82
110
  } else if value.is_duration_iso() {
@@ -1,7 +1,7 @@
1
1
  use std::fmt::Display;
2
2
  use std::sync::Arc;
3
3
 
4
- use calamine::{Data, Range, Rows, SheetType, SheetVisible};
4
+ use calamine::{Data, Dimensions, Range, Rows, SheetType, SheetVisible};
5
5
  use pyo3::class::basic::CompareOp;
6
6
  use pyo3::prelude::*;
7
7
  use pyo3::types::PyList;
@@ -25,7 +25,7 @@ pub enum SheetTypeEnum {
25
25
 
26
26
  impl Display for SheetTypeEnum {
27
27
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28
- write!(f, "SheetTypeEnum.{:?}", self)
28
+ write!(f, "SheetTypeEnum.{self:?}")
29
29
  }
30
30
  }
31
31
 
@@ -54,7 +54,7 @@ pub enum SheetVisibleEnum {
54
54
 
55
55
  impl Display for SheetVisibleEnum {
56
56
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57
- write!(f, "SheetVisibleEnum.{:?}", self)
57
+ write!(f, "SheetVisibleEnum.{self:?}")
58
58
  }
59
59
  }
60
60
 
@@ -68,6 +68,8 @@ impl From<SheetVisible> for SheetVisibleEnum {
68
68
  }
69
69
  }
70
70
 
71
+ type MergedCellRange = ((u32, u32), (u32, u32));
72
+
71
73
  #[pyclass]
72
74
  #[derive(Clone, PartialEq)]
73
75
  pub struct SheetMetadata {
@@ -132,13 +134,19 @@ pub struct CalamineSheet {
132
134
  #[pyo3(get)]
133
135
  name: String,
134
136
  range: Arc<Range<Data>>,
137
+ merged_cell_ranges: Option<Vec<Dimensions>>,
135
138
  }
136
139
 
137
140
  impl CalamineSheet {
138
- pub fn new(name: String, range: Range<Data>) -> Self {
141
+ pub fn new(
142
+ name: String,
143
+ range: Range<Data>,
144
+ merged_cell_ranges: Option<Vec<Dimensions>>,
145
+ ) -> Self {
139
146
  CalamineSheet {
140
147
  name,
141
148
  range: Arc::new(range),
149
+ merged_cell_ranges,
142
150
  }
143
151
  }
144
152
  }
@@ -212,6 +220,13 @@ impl CalamineSheet {
212
220
  fn iter_rows(&self) -> CalamineCellIterator {
213
221
  CalamineCellIterator::from_range(Arc::clone(&self.range))
214
222
  }
223
+
224
+ #[getter]
225
+ fn merged_cell_ranges(slf: PyRef<'_, Self>) -> Option<Vec<MergedCellRange>> {
226
+ slf.merged_cell_ranges
227
+ .as_ref()
228
+ .map(|r| r.iter().map(|d| (d.start, d.end)).collect())
229
+ }
215
230
  }
216
231
 
217
232
  #[pyclass]
@@ -2,7 +2,9 @@ use std::fs::File;
2
2
  use std::io::{BufReader, Cursor, Read};
3
3
  use std::path::PathBuf;
4
4
 
5
- use calamine::{open_workbook_auto, open_workbook_auto_from_rs, Reader, Sheets};
5
+ use calamine::{
6
+ open_workbook_auto, open_workbook_auto_from_rs, Error as CalamineCrateError, Reader, Sheets,
7
+ };
6
8
  use pyo3::exceptions::PyTypeError;
7
9
  use pyo3::prelude::*;
8
10
  use pyo3::types::{PyString, PyType};
@@ -44,6 +46,35 @@ impl SheetsEnum {
44
46
  SheetsEnum::None => Err(Error::WorkbookClosed),
45
47
  }
46
48
  }
49
+
50
+ fn worksheet_merge_cells(
51
+ &mut self,
52
+ name: &str,
53
+ ) -> Result<Option<Vec<calamine::Dimensions>>, Error> {
54
+ match self {
55
+ SheetsEnum::File(f) => match f {
56
+ Sheets::Xls(xls_f) => Ok(xls_f.worksheet_merge_cells(name)),
57
+ Sheets::Xlsx(xlsx_f) => xlsx_f
58
+ .worksheet_merge_cells(name)
59
+ .transpose()
60
+ .map_err(CalamineCrateError::Xlsx)
61
+ .map_err(Error::Calamine)
62
+ .map(|inner| inner.or(Some(Vec::new()))),
63
+ _ => Ok(None),
64
+ },
65
+ SheetsEnum::FileLike(f) => match f {
66
+ Sheets::Xls(xls_f) => Ok(xls_f.worksheet_merge_cells(name)),
67
+ Sheets::Xlsx(xlsx_f) => xlsx_f
68
+ .worksheet_merge_cells(name)
69
+ .transpose()
70
+ .map_err(CalamineCrateError::Xlsx)
71
+ .map_err(Error::Calamine)
72
+ .map(|inner| inner.or(Some(Vec::new()))),
73
+ _ => Ok(None),
74
+ },
75
+ SheetsEnum::None => Err(Error::WorkbookClosed),
76
+ }
77
+ }
47
78
  }
48
79
 
49
80
  #[pyclass]
@@ -61,7 +92,7 @@ pub struct CalamineWorkbook {
61
92
  impl CalamineWorkbook {
62
93
  fn __repr__(&self) -> PyResult<String> {
63
94
  match &self.path {
64
- Some(path) => Ok(format!("CalamineWorkbook(path='{}')", path)),
95
+ Some(path) => Ok(format!("CalamineWorkbook(path='{path}')")),
65
96
  None => Ok("CalamineWorkbook(path='bytes')".to_string()),
66
97
  }
67
98
  }
@@ -71,7 +102,7 @@ impl CalamineWorkbook {
71
102
  fn py_from_object(
72
103
  _cls: &Bound<'_, PyType>,
73
104
  py: Python<'_>,
74
- path_or_filelike: PyObject,
105
+ path_or_filelike: Py<PyAny>,
75
106
  ) -> PyResult<Self> {
76
107
  Self::from_object(py, path_or_filelike)
77
108
  }
@@ -81,22 +112,22 @@ impl CalamineWorkbook {
81
112
  fn py_from_filelike(
82
113
  _cls: &Bound<'_, PyType>,
83
114
  py: Python<'_>,
84
- filelike: PyObject,
115
+ filelike: Py<PyAny>,
85
116
  ) -> PyResult<Self> {
86
- py.allow_threads(|| Self::from_filelike(filelike))
117
+ py.detach(|| Self::from_filelike(filelike))
87
118
  }
88
119
 
89
120
  #[classmethod]
90
121
  #[pyo3(name = "from_path")]
91
- fn py_from_path(_cls: &Bound<'_, PyType>, py: Python<'_>, path: PyObject) -> PyResult<Self> {
122
+ fn py_from_path(_cls: &Bound<'_, PyType>, py: Python<'_>, path: Py<PyAny>) -> PyResult<Self> {
92
123
  if let Ok(string_ref) = path.downcast_bound::<PyString>(py) {
93
124
  let path = string_ref.to_string_lossy().to_string();
94
- return py.allow_threads(|| Self::from_path(&path));
125
+ return py.detach(|| Self::from_path(&path));
95
126
  }
96
127
 
97
128
  if let Ok(string_ref) = path.extract::<PathBuf>(py) {
98
129
  let path = string_ref.to_string_lossy().to_string();
99
- return py.allow_threads(|| Self::from_path(&path));
130
+ return py.detach(|| Self::from_path(&path));
100
131
  }
101
132
 
102
133
  Err(PyTypeError::new_err(""))
@@ -104,12 +135,12 @@ impl CalamineWorkbook {
104
135
 
105
136
  #[pyo3(name = "get_sheet_by_name")]
106
137
  fn py_get_sheet_by_name(&mut self, py: Python<'_>, name: &str) -> PyResult<CalamineSheet> {
107
- py.allow_threads(|| self.get_sheet_by_name(name))
138
+ py.detach(|| self.get_sheet_by_name(name))
108
139
  }
109
140
 
110
141
  #[pyo3(name = "get_sheet_by_index")]
111
142
  fn py_get_sheet_by_index(&mut self, py: Python<'_>, index: usize) -> PyResult<CalamineSheet> {
112
- py.allow_threads(|| self.get_sheet_by_index(index))
143
+ py.detach(|| self.get_sheet_by_index(index))
113
144
  }
114
145
 
115
146
  fn close(&mut self) -> PyResult<()> {
@@ -129,30 +160,30 @@ impl CalamineWorkbook {
129
160
 
130
161
  fn __exit__(
131
162
  &mut self,
132
- _exc_type: PyObject,
133
- _exc_value: PyObject,
134
- _traceback: PyObject,
163
+ _exc_type: Py<PyAny>,
164
+ _exc_value: Py<PyAny>,
165
+ _traceback: Py<PyAny>,
135
166
  ) -> PyResult<()> {
136
167
  self.close()
137
168
  }
138
169
  }
139
170
 
140
171
  impl CalamineWorkbook {
141
- pub fn from_object(py: Python<'_>, path_or_filelike: PyObject) -> PyResult<Self> {
172
+ pub fn from_object(py: Python<'_>, path_or_filelike: Py<PyAny>) -> PyResult<Self> {
142
173
  if let Ok(string_ref) = path_or_filelike.downcast_bound::<PyString>(py) {
143
174
  let path = string_ref.to_string_lossy().to_string();
144
- return py.allow_threads(|| Self::from_path(&path));
175
+ return py.detach(|| Self::from_path(&path));
145
176
  }
146
177
 
147
178
  if let Ok(string_ref) = path_or_filelike.extract::<PathBuf>(py) {
148
179
  let path = string_ref.to_string_lossy().to_string();
149
- return py.allow_threads(|| Self::from_path(&path));
180
+ return py.detach(|| Self::from_path(&path));
150
181
  }
151
182
 
152
- py.allow_threads(|| Self::from_filelike(path_or_filelike))
183
+ py.detach(|| Self::from_filelike(path_or_filelike))
153
184
  }
154
185
 
155
- pub fn from_filelike(filelike: PyObject) -> PyResult<Self> {
186
+ pub fn from_filelike(filelike: Py<PyAny>) -> PyResult<Self> {
156
187
  let mut buf = vec![];
157
188
  PyFileLikeObject::with_requirements(filelike, true, false, true, false)?
158
189
  .read_to_end(&mut buf)?;
@@ -192,14 +223,19 @@ impl CalamineWorkbook {
192
223
 
193
224
  fn get_sheet_by_name(&mut self, name: &str) -> PyResult<CalamineSheet> {
194
225
  let range = self.sheets.worksheet_range(name).map_err(err_to_py)?;
195
- Ok(CalamineSheet::new(name.to_owned(), range))
226
+ let merge_cells_range = self.sheets.worksheet_merge_cells(name).map_err(err_to_py)?;
227
+ Ok(CalamineSheet::new(
228
+ name.to_owned(),
229
+ range,
230
+ merge_cells_range,
231
+ ))
196
232
  }
197
233
 
198
234
  fn get_sheet_by_index(&mut self, index: usize) -> PyResult<CalamineSheet> {
199
235
  let name = self
200
236
  .sheet_names
201
237
  .get(index)
202
- .ok_or_else(|| WorksheetNotFound::new_err(format!("Worksheet '{}' not found", index)))?
238
+ .ok_or_else(|| WorksheetNotFound::new_err(format!("Worksheet '{index}' not found")))?
203
239
  .to_string();
204
240
  self.get_sheet_by_name(&name)
205
241
  }
@@ -15,7 +15,7 @@ PATH = Path(__file__).parent / "data"
15
15
 
16
16
 
17
17
  def test_ods_read():
18
- names = ["Sheet1", "Sheet2"]
18
+ names = ["Sheet1", "Sheet2", "Merged Cells"]
19
19
  data = [
20
20
  ["", "", "", "", "", "", "", "", "", ""],
21
21
  [
@@ -59,9 +59,11 @@ def test_ods_read():
59
59
  assert [] == reader.get_sheet_by_index(1).to_python()
60
60
  assert [] == reader.get_sheet_by_index(1).to_python(skip_empty_area=False)
61
61
 
62
+ assert None is reader.get_sheet_by_name("Merged Cells").merged_cell_ranges
63
+
62
64
 
63
65
  def test_xls_read():
64
- names = ["Sheet1", "Sheet2"]
66
+ names = ["Sheet1", "Sheet2", "Merged Cells"]
65
67
  data = [
66
68
  ["", "", "", "", "", "", "", "", "", ""],
67
69
  [
@@ -101,9 +103,13 @@ def test_xls_read():
101
103
  assert [] == reader.get_sheet_by_index(1).to_python()
102
104
  assert [] == reader.get_sheet_by_index(1).to_python(skip_empty_area=False)
103
105
 
106
+ assert [((0, 0), (1, 0)), ((0, 1), (1, 1))] == reader.get_sheet_by_name(
107
+ "Merged Cells"
108
+ ).merged_cell_ranges
109
+
104
110
 
105
111
  def test_xlsb_read():
106
- names = ["Sheet1", "Sheet2", "Sheet3"]
112
+ names = ["Sheet1", "Sheet2", "Sheet3", "Merged Cells"]
107
113
  data = [
108
114
  ["", "", "", "", "", "", "", "", "", ""],
109
115
  [
@@ -143,9 +149,11 @@ def test_xlsb_read():
143
149
  assert [] == reader.get_sheet_by_index(1).to_python()
144
150
  assert [] == reader.get_sheet_by_index(1).to_python(skip_empty_area=False)
145
151
 
152
+ assert None is reader.get_sheet_by_name("Merged Cells").merged_cell_ranges
153
+
146
154
 
147
155
  def test_xlsx_read():
148
- names = ["Sheet1", "Sheet2", "Sheet3"]
156
+ names = ["Sheet1", "Sheet2", "Sheet3", "Merged Cells"]
149
157
  data = [
150
158
  ["", "", "", "", "", "", "", "", "", ""],
151
159
  [
@@ -185,9 +193,13 @@ def test_xlsx_read():
185
193
  assert [] == reader.get_sheet_by_index(1).to_python()
186
194
  assert [] == reader.get_sheet_by_index(1).to_python(skip_empty_area=False)
187
195
 
196
+ assert [((0, 0), (1, 0)), ((0, 1), (1, 1))] == reader.get_sheet_by_name(
197
+ "Merged Cells"
198
+ ).merged_cell_ranges
199
+
188
200
 
189
201
  def test_xlsx_iter_rows():
190
- names = ["Sheet1", "Sheet2", "Sheet3"]
202
+ names = ["Sheet1", "Sheet2", "Sheet3", "Merged Cells"]
191
203
  data = [
192
204
  ["", "", "", "", "", "", "", "", "", ""],
193
205
  [
@@ -363,3 +375,10 @@ def test_close_workbook_double():
363
375
 
364
376
  with pytest.raises(WorkbookClosed):
365
377
  reader.close()
378
+
379
+
380
+ def test_datetime_overflow():
381
+ reader = CalamineWorkbook.from_path(PATH / "issue139.xlsx")
382
+
383
+ # 2994626.0 = 10099-01-01
384
+ assert reader.get_sheet_by_index(0).to_python() == [["date"], [2994626.0]]
@@ -1,8 +1,6 @@
1
- from importlib.metadata import version
2
1
  from pathlib import Path
3
2
 
4
3
  import pytest
5
- from packaging.version import Version, parse
6
4
 
7
5
  try:
8
6
  import pandas as pd
@@ -14,17 +12,6 @@ except ImportError:
14
12
  PATH = Path(__file__).parent / "data"
15
13
 
16
14
 
17
- @pytest.fixture(scope="module", autouse=True)
18
- def pandas_monkeypatch():
19
- if parse(version("pandas")) >= Version("2.2.0"):
20
- yield
21
- else:
22
- from python_calamine.pandas import pandas_monkeypatch
23
-
24
- pandas_monkeypatch()
25
- yield
26
-
27
-
28
15
  @pytest.mark.skipif(not pd, reason="pandas is required")
29
16
  def test_ods_pandas(expected_df_ods):
30
17
  result = pd.read_excel(PATH / "base.ods", sheet_name="Sheet1", engine="calamine")
@@ -1,27 +0,0 @@
1
- [package]
2
- name = "python-calamine"
3
- version = "0.3.2"
4
- authors = ["Dmitriy <dimastbk@proton.me>"]
5
- description = "Python binding for Rust's library for reading excel and odf file - calamine"
6
- edition = "2021"
7
- homepage = "https://github.com/dimastbk/python-calamine"
8
- license = "MIT"
9
- readme = "README.md"
10
- repository = "https://github.com/dimastbk/python-calamine"
11
-
12
- [lib]
13
- name = "python_calamine"
14
- crate-type = ["cdylib"]
15
-
16
- [dependencies]
17
- calamine = { git = "https://github.com/tafia/calamine.git", rev = "5cda5c734661f656bdfb7ac7a1b95e77bf89aa09", features = ["dates"] }
18
- pyo3 = { version = "0.24.0", features = [
19
- "extension-module",
20
- "chrono",
21
- "generate-import-lib",
22
- ] }
23
- chrono = { version = "0.4.38", features = ["serde"] }
24
- pyo3-file = "0.12.0"
25
-
26
- [build-dependencies]
27
- pyo3-build-config = "0.24.0"
@@ -1,136 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from datetime import date, datetime, time, timedelta
4
- from importlib.metadata import version
5
- from typing import TYPE_CHECKING, Union, cast
6
-
7
- import pandas as pd
8
- from packaging.version import Version, parse
9
- from pandas._typing import Scalar
10
- from pandas.compat._optional import import_optional_dependency
11
- from pandas.core.shared_docs import _shared_docs
12
- from pandas.io.excel import ExcelFile
13
- from pandas.io.excel._base import ( # type:ignore[attr-defined] # missing in pandas-stubs
14
- BaseExcelReader,
15
- )
16
- from pandas.util._decorators import ( # type:ignore[attr-defined] # missing in pandas-stubs
17
- doc,
18
- )
19
-
20
- if TYPE_CHECKING:
21
- from pandas._typing import FilePath, ReadBuffer, StorageOptions
22
- from python_calamine import CalamineSheet, CalamineWorkbook
23
-
24
- _CellValueT = Union[int, float, str, bool, time, date, datetime, timedelta]
25
-
26
-
27
- PANDAS_VERSION = parse(version("pandas"))
28
-
29
-
30
- class CalamineExcelReader(BaseExcelReader):
31
- book: CalamineWorkbook
32
-
33
- @doc(storage_options=_shared_docs["storage_options"])
34
- def __init__(
35
- self,
36
- filepath_or_buffer: FilePath | ReadBuffer[bytes],
37
- storage_options: StorageOptions | None = None,
38
- engine_kwargs: dict | None = None,
39
- ) -> None:
40
- """
41
- Reader using calamine engine (xlsx/xls/xlsb/ods).
42
-
43
- Parameters
44
- ----------
45
- filepath_or_buffer : str, path to be parsed or
46
- an open readable stream.
47
- {storage_options}
48
- engine_kwargs : dict, optional
49
- Arbitrary keyword arguments passed to excel engine.
50
- """
51
- import_optional_dependency("python_calamine")
52
- if PANDAS_VERSION >= Version("2.2.0"):
53
- raise ValueError("Pandas >= 2.2.0 has builtin support of calamine")
54
- elif PANDAS_VERSION >= Version("2.1.0"):
55
- super().__init__(
56
- filepath_or_buffer,
57
- storage_options=storage_options,
58
- engine_kwargs=engine_kwargs,
59
- )
60
- elif PANDAS_VERSION >= Version("2.0.0"):
61
- super().__init__(
62
- filepath_or_buffer,
63
- storage_options=storage_options,
64
- )
65
- else:
66
- raise ValueError("Pandas >= 2 is only supported")
67
-
68
- @property
69
- def _workbook_class(self) -> type[CalamineWorkbook]:
70
- from python_calamine import CalamineWorkbook
71
-
72
- return CalamineWorkbook
73
-
74
- def load_workbook(
75
- self,
76
- filepath_or_buffer: FilePath | ReadBuffer[bytes],
77
- engine_kwargs: dict | None = None,
78
- ) -> CalamineWorkbook:
79
- from python_calamine import load_workbook
80
-
81
- return load_workbook(filepath_or_buffer, **(engine_kwargs or {}))
82
-
83
- @property
84
- def sheet_names(self) -> list[str]:
85
- from python_calamine import SheetTypeEnum
86
-
87
- return [
88
- sheet.name
89
- for sheet in self.book.sheets_metadata
90
- if sheet.typ == SheetTypeEnum.WorkSheet
91
- ]
92
-
93
- def get_sheet_by_name(self, name: str) -> CalamineSheet:
94
- self.raise_if_bad_sheet_by_name(name)
95
- return self.book.get_sheet_by_name(name)
96
-
97
- def get_sheet_by_index(self, index: int) -> CalamineSheet:
98
- self.raise_if_bad_sheet_by_index(index)
99
- return self.book.get_sheet_by_index(index)
100
-
101
- def get_sheet_data(
102
- self, sheet: CalamineSheet, file_rows_needed: int | None = None
103
- ) -> list[list[Scalar]]:
104
- def _convert_cell(value: _CellValueT) -> Scalar:
105
- if isinstance(value, float):
106
- val = int(value)
107
- if val == value:
108
- return val
109
- else:
110
- return value
111
- elif isinstance(value, date):
112
- return pd.Timestamp(value)
113
- elif isinstance(value, timedelta):
114
- return pd.Timedelta(value)
115
- elif isinstance(value, time):
116
- # cast needed here because Scalar doesn't include datetime.time
117
- return cast(Scalar, value)
118
-
119
- return value
120
-
121
- rows: list[list[_CellValueT]] = sheet.to_python(skip_empty_area=False)
122
- data: list[list[Scalar]] = []
123
-
124
- for row in rows:
125
- data.append([_convert_cell(cell) for cell in row])
126
- if file_rows_needed is not None and len(data) >= file_rows_needed:
127
- break
128
-
129
- return data
130
-
131
-
132
- def pandas_monkeypatch() -> None:
133
- ExcelFile._engines = { # type:ignore[attr-defined] # missing in pandas-stubs
134
- "calamine": CalamineExcelReader,
135
- **ExcelFile._engines, # type:ignore[attr-defined] # missing in pandas-stubs
136
- }
Binary file
Binary file
Binary file
Binary file
File without changes