numbers-parser 4.4.6__py3-none-any.whl → 4.4.8__py3-none-any.whl
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.
- numbers_parser/__init__.py +7 -5
- numbers_parser/_cat_numbers.py +1 -2
- numbers_parser/_unpack_numbers.py +14 -16
- numbers_parser/bullets.py +1 -1
- numbers_parser/cell.py +33 -29
- numbers_parser/cell_storage.py +36 -29
- numbers_parser/constants.py +2 -2
- numbers_parser/containers.py +12 -14
- numbers_parser/document.py +54 -45
- numbers_parser/exceptions.py +7 -7
- numbers_parser/file.py +18 -8
- numbers_parser/formula.py +7 -3
- numbers_parser/iwafile.py +23 -27
- numbers_parser/mapping.py +24 -24
- numbers_parser/model.py +112 -131
- numbers_parser/numbers_cache.py +3 -2
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/METADATA +8 -5
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/RECORD +21 -21
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/WHEEL +0 -0
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/entry_points.txt +0 -0
numbers_parser/__init__.py
CHANGED
|
@@ -23,9 +23,9 @@ import os
|
|
|
23
23
|
import plistlib
|
|
24
24
|
import warnings
|
|
25
25
|
|
|
26
|
-
from numbers_parser.
|
|
27
|
-
from numbers_parser.
|
|
28
|
-
from numbers_parser.exceptions import *
|
|
26
|
+
from numbers_parser.cell import *
|
|
27
|
+
from numbers_parser.document import Document
|
|
28
|
+
from numbers_parser.exceptions import *
|
|
29
29
|
|
|
30
30
|
__version__ = importlib.metadata.version("numbers-parser")
|
|
31
31
|
|
|
@@ -53,12 +53,14 @@ def _get_version():
|
|
|
53
53
|
def _check_installed_numbers_version():
|
|
54
54
|
try:
|
|
55
55
|
fp = open(os.path.join(_DEFAULT_NUMBERS_INSTALL_PATH, _VERSION_PLIST_PATH), "rb")
|
|
56
|
-
except
|
|
56
|
+
except OSError:
|
|
57
57
|
return None
|
|
58
58
|
version_dict = plistlib.load(fp)
|
|
59
59
|
installed_version = version_dict["CFBundleShortVersionString"]
|
|
60
60
|
if installed_version not in _SUPPORTED_NUMBERS_VERSIONS:
|
|
61
|
-
warnings.warn(
|
|
61
|
+
warnings.warn(
|
|
62
|
+
f"Numbers version {installed_version} not tested with this version", stacklevel=2
|
|
63
|
+
)
|
|
62
64
|
fp.close()
|
|
63
65
|
return installed_version
|
|
64
66
|
|
numbers_parser/_cat_numbers.py
CHANGED
|
@@ -5,9 +5,8 @@ import sys
|
|
|
5
5
|
|
|
6
6
|
import sigfig
|
|
7
7
|
|
|
8
|
-
from numbers_parser import Document, ErrorCell, FileFormatError, NumberCell,
|
|
8
|
+
from numbers_parser import Document, ErrorCell, FileError, FileFormatError, NumberCell, _get_version
|
|
9
9
|
from numbers_parser import __name__ as numbers_parser_name
|
|
10
|
-
from numbers_parser import _get_version
|
|
11
10
|
from numbers_parser.constants import MAX_SIGNIFICANT_DIGITS
|
|
12
11
|
from numbers_parser.experimental import _enable_experimental_features
|
|
13
12
|
|
|
@@ -1,33 +1,31 @@
|
|
|
1
|
-
import
|
|
1
|
+
import contextlib
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
import
|
|
4
|
+
import os
|
|
5
5
|
import sys
|
|
6
|
-
|
|
7
|
-
from array import array
|
|
8
6
|
from argparse import ArgumentParser
|
|
7
|
+
from array import array
|
|
9
8
|
from base64 import b64decode
|
|
10
9
|
from binascii import hexlify
|
|
10
|
+
|
|
11
|
+
import regex
|
|
11
12
|
from compact_json import Formatter
|
|
12
13
|
|
|
13
|
-
from numbers_parser.file import read_numbers_file
|
|
14
|
-
from numbers_parser import _get_version
|
|
15
14
|
from numbers_parser import __name__ as numbers_parser_name
|
|
15
|
+
from numbers_parser import _get_version
|
|
16
|
+
from numbers_parser.exceptions import FileError, FileFormatError, UnsupportedError
|
|
17
|
+
from numbers_parser.file import read_numbers_file
|
|
16
18
|
from numbers_parser.iwafile import IWAFile
|
|
17
|
-
from numbers_parser.exceptions import FileFormatError, UnsupportedError, FileError
|
|
18
19
|
from numbers_parser.numbers_uuid import NumbersUUID
|
|
19
20
|
|
|
20
|
-
|
|
21
21
|
logger = logging.getLogger(numbers_parser_name)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def ensure_directory_exists(prefix, path):
|
|
25
25
|
"""Ensure that a path's directory exists."""
|
|
26
26
|
parts = os.path.split(path)
|
|
27
|
-
|
|
27
|
+
with contextlib.suppress(OSError):
|
|
28
28
|
os.makedirs(os.path.join(*([prefix] + list(parts[:-1]))))
|
|
29
|
-
except OSError:
|
|
30
|
-
pass
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
def prettify_uuids(obj):
|
|
@@ -54,18 +52,18 @@ def prettify_uuids(obj):
|
|
|
54
52
|
def prettify_cell_storage(obj):
|
|
55
53
|
if isinstance(obj, dict):
|
|
56
54
|
for k, v in obj.items():
|
|
57
|
-
if isinstance(v, dict
|
|
55
|
+
if isinstance(v, (dict, list)):
|
|
58
56
|
prettify_cell_storage(v)
|
|
59
|
-
elif k
|
|
57
|
+
elif k in ["cell_storage_buffer", "cell_storage_buffer_pre_bnc"]:
|
|
60
58
|
obj[k] = str(hexlify(b64decode(obj[k]), sep=":"))
|
|
61
59
|
obj[k] = obj[k].replace("b'", "").replace("'", "")
|
|
62
|
-
elif k
|
|
60
|
+
elif k in ["cell_offsets", k == "cell_offsets_pre_bnc"]:
|
|
63
61
|
offsets = array("h", b64decode(obj[k])).tolist()
|
|
64
62
|
obj[k] = ",".join([str(x) for x in offsets])
|
|
65
63
|
obj[k] = regex.sub(r"(?:,-1)+$", ",[...]", obj[k])
|
|
66
64
|
else: # list
|
|
67
65
|
for v in obj:
|
|
68
|
-
if isinstance(v, dict
|
|
66
|
+
if isinstance(v, (dict, list)):
|
|
69
67
|
prettify_cell_storage(v)
|
|
70
68
|
|
|
71
69
|
|
|
@@ -137,7 +135,7 @@ def main():
|
|
|
137
135
|
try:
|
|
138
136
|
read_numbers_file(
|
|
139
137
|
document,
|
|
140
|
-
file_handler=lambda filename, blob: process_file(
|
|
138
|
+
file_handler=lambda filename, blob, output_dir=output_dir: process_file(
|
|
141
139
|
filename, blob, output_dir, args
|
|
142
140
|
),
|
|
143
141
|
)
|
numbers_parser/bullets.py
CHANGED
numbers_parser/cell.py
CHANGED
|
@@ -29,7 +29,7 @@ from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
|
|
|
29
29
|
from numbers_parser.generated.TSWPArchives_pb2 import (
|
|
30
30
|
ParagraphStylePropertiesArchive as ParagraphStyle,
|
|
31
31
|
)
|
|
32
|
-
from numbers_parser.numbers_cache import
|
|
32
|
+
from numbers_parser.numbers_cache import Cacheable, cache
|
|
33
33
|
|
|
34
34
|
__all__ = [
|
|
35
35
|
"Alignment",
|
|
@@ -122,8 +122,7 @@ class Alignment(_Alignment):
|
|
|
122
122
|
raise TypeError("invalid vertical alignment")
|
|
123
123
|
vertical = VERTICAL_MAP[vertical]
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
return self
|
|
125
|
+
return super(_Alignment, cls).__new__(cls, (horizontal, vertical))
|
|
127
126
|
|
|
128
127
|
|
|
129
128
|
DEFAULT_ALIGNMENT_CLASS = Alignment(*DEFAULT_ALIGNMENT)
|
|
@@ -186,13 +185,11 @@ class Style:
|
|
|
186
185
|
|
|
187
186
|
@classmethod
|
|
188
187
|
def from_storage(cls, cell_storage: object, model: object):
|
|
189
|
-
style = Style()
|
|
190
|
-
|
|
191
188
|
if cell_storage.image_data is not None:
|
|
192
189
|
bg_image = BackgroundImage(*cell_storage.image_data)
|
|
193
190
|
else:
|
|
194
191
|
bg_image = None
|
|
195
|
-
|
|
192
|
+
return Style(
|
|
196
193
|
alignment=model.cell_alignment(cell_storage),
|
|
197
194
|
bg_image=bg_image,
|
|
198
195
|
bg_color=model.cell_bg_color(cell_storage),
|
|
@@ -212,7 +209,6 @@ class Style:
|
|
|
212
209
|
_text_style_obj_id=model.text_style_object_id(cell_storage),
|
|
213
210
|
_cell_style_obj_id=model.cell_style_object_id(cell_storage),
|
|
214
211
|
)
|
|
215
|
-
return style
|
|
216
212
|
|
|
217
213
|
def __post_init__(self):
|
|
218
214
|
self.bg_color = rgb_color(self.bg_color)
|
|
@@ -229,7 +225,8 @@ class Style:
|
|
|
229
225
|
|
|
230
226
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
231
227
|
"""Detect changes to cell styles and flag the style for
|
|
232
|
-
possible updates when saving the document
|
|
228
|
+
possible updates when saving the document.
|
|
229
|
+
"""
|
|
233
230
|
if name in ["bg_color", "font_color"]:
|
|
234
231
|
value = rgb_color(value)
|
|
235
232
|
if name == "alignment":
|
|
@@ -244,13 +241,13 @@ class Style:
|
|
|
244
241
|
|
|
245
242
|
|
|
246
243
|
def rgb_color(color) -> RGB:
|
|
247
|
-
"""Raise a TypeError if a color is not a valid RGB value"""
|
|
244
|
+
"""Raise a TypeError if a color is not a valid RGB value."""
|
|
248
245
|
if color is None:
|
|
249
246
|
return None
|
|
250
247
|
if isinstance(color, RGB):
|
|
251
248
|
return color
|
|
252
249
|
if isinstance(color, tuple):
|
|
253
|
-
if not (len(color) == 3 and all(
|
|
250
|
+
if not (len(color) == 3 and all(isinstance(x, int) for x in color)):
|
|
254
251
|
raise TypeError("RGB color must be an RGB or a tuple of 3 integers")
|
|
255
252
|
return RGB(*color)
|
|
256
253
|
elif isinstance(color, list):
|
|
@@ -259,13 +256,13 @@ def rgb_color(color) -> RGB:
|
|
|
259
256
|
|
|
260
257
|
|
|
261
258
|
def alignment(value) -> Alignment:
|
|
262
|
-
"""Raise a TypeError if a alignment is not a valid"""
|
|
259
|
+
"""Raise a TypeError if a alignment is not a valid."""
|
|
263
260
|
if value is None:
|
|
264
261
|
return Alignment()
|
|
265
262
|
if isinstance(value, Alignment):
|
|
266
263
|
return value
|
|
267
264
|
if isinstance(value, tuple):
|
|
268
|
-
if not (len(value) == 2 and all(
|
|
265
|
+
if not (len(value) == 2 and all(isinstance(x, (int, str)) for x in value)):
|
|
269
266
|
raise TypeError("Alignment must be an Alignment or a tuple of 2 integers/strings")
|
|
270
267
|
return Alignment(*value)
|
|
271
268
|
raise TypeError("Alignment must be an Alignment or a tuple of 2 integers/strings")
|
|
@@ -285,16 +282,20 @@ class Border:
|
|
|
285
282
|
def __init__(
|
|
286
283
|
self,
|
|
287
284
|
width: float = DEFAULT_BORDER_WIDTH,
|
|
288
|
-
color: RGB =
|
|
289
|
-
style: BorderType =
|
|
285
|
+
color: RGB = None,
|
|
286
|
+
style: BorderType = None,
|
|
290
287
|
_order: int = 0,
|
|
291
288
|
):
|
|
292
289
|
if not isinstance(width, float):
|
|
293
290
|
raise TypeError("width must be a float number of points")
|
|
294
291
|
self.width = width
|
|
295
292
|
|
|
293
|
+
if color is None:
|
|
294
|
+
color = RGB(*DEFAULT_BORDER_COLOR)
|
|
296
295
|
self.color = rgb_color(color)
|
|
297
296
|
|
|
297
|
+
if style is None:
|
|
298
|
+
style = BorderType(BORDER_STYLE_MAP[DEFAULT_BORDER_STYLE])
|
|
298
299
|
if isinstance(style, str):
|
|
299
300
|
style = style.lower()
|
|
300
301
|
if style not in BORDER_STYLE_MAP:
|
|
@@ -394,14 +395,14 @@ class CellBorder:
|
|
|
394
395
|
|
|
395
396
|
|
|
396
397
|
class MergeReference:
|
|
397
|
-
"""Cell reference for cells eliminated by a merge"""
|
|
398
|
+
"""Cell reference for cells eliminated by a merge."""
|
|
398
399
|
|
|
399
400
|
def __init__(self, row_start: int, col_start: int, row_end: int, col_end: int):
|
|
400
401
|
self.rect = (row_start, col_start, row_end, col_end)
|
|
401
402
|
|
|
402
403
|
|
|
403
404
|
class MergeAnchor:
|
|
404
|
-
"""Cell reference for the merged cell"""
|
|
405
|
+
"""Cell reference for the merged cell."""
|
|
405
406
|
|
|
406
407
|
def __init__(self, size: Tuple):
|
|
407
408
|
self.size = size
|
|
@@ -475,11 +476,12 @@ class Cell(Cacheable):
|
|
|
475
476
|
warn(
|
|
476
477
|
f"'{value}' rounded to {MAX_SIGNIFICANT_DIGITS} significant digits",
|
|
477
478
|
RuntimeWarning,
|
|
479
|
+
stacklevel=2,
|
|
478
480
|
)
|
|
479
481
|
return NumberCell(row_num, col_num, rounded_value)
|
|
480
|
-
elif isinstance(value,
|
|
482
|
+
elif isinstance(value, (DateTime, builtin_datetime)):
|
|
481
483
|
return DateCell(row_num, col_num, pendulum_instance(value))
|
|
482
|
-
elif isinstance(value,
|
|
484
|
+
elif isinstance(value, (Duration, builtin_timedelta)):
|
|
483
485
|
return DurationCell(row_num, col_num, value)
|
|
484
486
|
else:
|
|
485
487
|
raise ValueError("Can't determine cell type from type " + type(value).__name__)
|
|
@@ -527,6 +529,7 @@ class Cell(Cacheable):
|
|
|
527
529
|
"image_filename is deprecated and will be removed in the future. "
|
|
528
530
|
+ "Please use the style property",
|
|
529
531
|
DeprecationWarning,
|
|
532
|
+
stacklevel=2,
|
|
530
533
|
)
|
|
531
534
|
if self.style is not None and self.style.bg_image is not None:
|
|
532
535
|
return self.style.bg_image.filename
|
|
@@ -539,6 +542,7 @@ class Cell(Cacheable):
|
|
|
539
542
|
"image_data is deprecated and will be removed in the future. "
|
|
540
543
|
+ "Please use the style property",
|
|
541
544
|
DeprecationWarning,
|
|
545
|
+
stacklevel=2,
|
|
542
546
|
)
|
|
543
547
|
if self.style is not None and self.style.bg_image is not None:
|
|
544
548
|
return self.style.bg_image.data
|
|
@@ -555,8 +559,7 @@ class Cell(Cacheable):
|
|
|
555
559
|
def formula(self):
|
|
556
560
|
if self._formula_key is not None:
|
|
557
561
|
table_formulas = self._model.table_formulas(self._table_id)
|
|
558
|
-
|
|
559
|
-
return formula
|
|
562
|
+
return table_formulas.formula(self._formula_key, self.row, self.col)
|
|
560
563
|
else:
|
|
561
564
|
return None
|
|
562
565
|
|
|
@@ -580,7 +583,11 @@ class Cell(Cacheable):
|
|
|
580
583
|
|
|
581
584
|
@style.setter
|
|
582
585
|
def style(self, _):
|
|
583
|
-
warn(
|
|
586
|
+
warn(
|
|
587
|
+
"cell style cannot be set; use Table.set_cell_style() instead",
|
|
588
|
+
UnsupportedWarning,
|
|
589
|
+
stacklevel=2,
|
|
590
|
+
)
|
|
584
591
|
|
|
585
592
|
@property
|
|
586
593
|
def border(self):
|
|
@@ -592,6 +599,7 @@ class Cell(Cacheable):
|
|
|
592
599
|
warn(
|
|
593
600
|
"cell border values cannot be set; use Table.set_cell_border() instead",
|
|
594
601
|
UnsupportedWarning,
|
|
602
|
+
stacklevel=2,
|
|
595
603
|
)
|
|
596
604
|
|
|
597
605
|
|
|
@@ -718,8 +726,7 @@ range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
|
|
|
718
726
|
|
|
719
727
|
|
|
720
728
|
def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
721
|
-
"""
|
|
722
|
-
Convert a cell reference in A1 notation to a zero indexed row and column.
|
|
729
|
+
"""Convert a cell reference in A1 notation to a zero indexed row and column.
|
|
723
730
|
Args:
|
|
724
731
|
cell_str: A1 style string.
|
|
725
732
|
Returns:
|
|
@@ -750,8 +757,7 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
|
750
757
|
|
|
751
758
|
|
|
752
759
|
def xl_range(first_row, first_col, last_row, last_col):
|
|
753
|
-
"""
|
|
754
|
-
Convert zero indexed row and col cell references to a A1:B1 range string.
|
|
760
|
+
"""Convert zero indexed row and col cell references to a A1:B1 range string.
|
|
755
761
|
Args:
|
|
756
762
|
first_row: The first cell row. Int.
|
|
757
763
|
first_col: The first cell column. Int.
|
|
@@ -770,8 +776,7 @@ def xl_range(first_row, first_col, last_row, last_col):
|
|
|
770
776
|
|
|
771
777
|
|
|
772
778
|
def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
773
|
-
"""
|
|
774
|
-
Convert a zero indexed row and column cell reference to a A1 style string.
|
|
779
|
+
"""Convert a zero indexed row and column cell reference to a A1 style string.
|
|
775
780
|
Args:
|
|
776
781
|
row: The cell row. Int.
|
|
777
782
|
col: The cell column. Int.
|
|
@@ -795,8 +800,7 @@ def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
|
795
800
|
|
|
796
801
|
|
|
797
802
|
def xl_col_to_name(col, col_abs=False):
|
|
798
|
-
"""
|
|
799
|
-
Convert a zero indexed column cell reference to a string.
|
|
803
|
+
"""Convert a zero indexed column cell reference to a string.
|
|
800
804
|
Args:
|
|
801
805
|
col: The cell column. Int.
|
|
802
806
|
col_abs: Optional flag to make the column absolute. Bool.
|
numbers_parser/cell_storage.py
CHANGED
|
@@ -27,7 +27,7 @@ from numbers_parser.constants import (
|
|
|
27
27
|
)
|
|
28
28
|
from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
|
|
29
29
|
from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
|
|
30
|
-
from numbers_parser.numbers_cache import
|
|
30
|
+
from numbers_parser.numbers_cache import Cacheable, cache
|
|
31
31
|
from numbers_parser.numbers_uuid import NumbersUUID
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger(numbers_parser_name)
|
|
@@ -111,7 +111,9 @@ class CellStorage(Cacheable):
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
# @profile
|
|
114
|
-
def __init__(
|
|
114
|
+
def __init__( # noqa: PLR0912, PLR0913, PLR0915
|
|
115
|
+
self, model: object, table_id: int, buffer, row_num, col_num
|
|
116
|
+
):
|
|
115
117
|
self.buffer = buffer
|
|
116
118
|
self.model = model
|
|
117
119
|
self.table_id = table_id
|
|
@@ -277,7 +279,7 @@ class CellStorage(Cacheable):
|
|
|
277
279
|
@property
|
|
278
280
|
@cache(num_args=0)
|
|
279
281
|
def image_data(self) -> Tuple[bytes, str]:
|
|
280
|
-
"""Return the background image data for a cell or None if no image"""
|
|
282
|
+
"""Return the background image data for a cell or None if no image."""
|
|
281
283
|
if self.cell_style_id is None:
|
|
282
284
|
return None
|
|
283
285
|
style = self.model.table_style(self.table_id, self.cell_style_id)
|
|
@@ -286,10 +288,18 @@ class CellStorage(Cacheable):
|
|
|
286
288
|
|
|
287
289
|
image_id = style.cell_properties.cell_fill.image.imagedata.identifier
|
|
288
290
|
datas = self.model.objects[PACKAGE_ID].datas
|
|
289
|
-
|
|
291
|
+
stored_filename = [x.file_name for x in datas if x.identifier == image_id][0]
|
|
292
|
+
preferred_filename = [x.preferred_file_name for x in datas if x.identifier == image_id][0]
|
|
290
293
|
all_paths = self.model.objects.file_store.keys()
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
image_pathnames = [x for x in all_paths if x == f"Data/{stored_filename}"]
|
|
295
|
+
if len(image_pathnames) == 0:
|
|
296
|
+
warn(
|
|
297
|
+
f"Cannot find file '{preferred_filename}' in Numbers archive",
|
|
298
|
+
RuntimeWarning,
|
|
299
|
+
stacklevel=3,
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
return (self.model.objects.file_store[image_pathnames[0]], preferred_filename)
|
|
293
303
|
|
|
294
304
|
def custom_format(self) -> str:
|
|
295
305
|
if self.text_format_id is not None and self.type == CellType.TEXT:
|
|
@@ -341,14 +351,16 @@ class CellStorage(Cacheable):
|
|
|
341
351
|
formatted_value = decode_date_format(custom_format_string, self.datetime)
|
|
342
352
|
else:
|
|
343
353
|
warn(
|
|
344
|
-
f"Unexpected custom format type {custom_format.format_type}",
|
|
354
|
+
f"Unexpected custom format type {custom_format.format_type}",
|
|
355
|
+
UnsupportedWarning,
|
|
356
|
+
stacklevel=3,
|
|
345
357
|
)
|
|
346
358
|
return ""
|
|
347
359
|
else:
|
|
348
360
|
formatted_value = decode_date_format(format.date_time_format, self.datetime)
|
|
349
361
|
return formatted_value
|
|
350
362
|
|
|
351
|
-
def duration_format(self) -> str:
|
|
363
|
+
def duration_format(self) -> str:
|
|
352
364
|
format = self.model.table_format(self.table_id, self.duration_format_id)
|
|
353
365
|
|
|
354
366
|
duration_style = format.duration_style
|
|
@@ -433,7 +445,7 @@ def unpack_decimal128(buffer: bytearray) -> float:
|
|
|
433
445
|
|
|
434
446
|
|
|
435
447
|
def days_occurred_in_month(value: datetime) -> str:
|
|
436
|
-
"""Return how many times the day of the datetime value has fallen in the month"""
|
|
448
|
+
"""Return how many times the day of the datetime value has fallen in the month."""
|
|
437
449
|
n_days = int((value - value.replace(day=1)).days / 7) + 1
|
|
438
450
|
return str(n_days)
|
|
439
451
|
|
|
@@ -446,12 +458,12 @@ def decode_date_format_field(field: str, value: datetime) -> str:
|
|
|
446
458
|
else:
|
|
447
459
|
return value.strftime(s)
|
|
448
460
|
else:
|
|
449
|
-
warn(f"Unsupported field code '{field}'", UnsupportedWarning)
|
|
461
|
+
warn(f"Unsupported field code '{field}'", UnsupportedWarning, stacklevel=4)
|
|
450
462
|
return ""
|
|
451
463
|
|
|
452
464
|
|
|
453
465
|
def decode_date_format(format, value):
|
|
454
|
-
"""Parse a custom date format string and return a formatted datetime value"""
|
|
466
|
+
"""Parse a custom date format string and return a formatted datetime value."""
|
|
455
467
|
chars = [*format]
|
|
456
468
|
index = 0
|
|
457
469
|
in_string = False
|
|
@@ -498,8 +510,8 @@ def decode_date_format(format, value):
|
|
|
498
510
|
return result
|
|
499
511
|
|
|
500
512
|
|
|
501
|
-
def decode_text_format(format, value: str):
|
|
502
|
-
"""Parse a custom date format string and return a formatted number value"""
|
|
513
|
+
def decode_text_format(format, value: str):
|
|
514
|
+
"""Parse a custom date format string and return a formatted number value."""
|
|
503
515
|
custom_format_string = format.custom_format_string
|
|
504
516
|
return custom_format_string.replace("\ue421", value)
|
|
505
517
|
|
|
@@ -530,8 +542,8 @@ def expand_quotes(value: str) -> str:
|
|
|
530
542
|
return formatted_value
|
|
531
543
|
|
|
532
544
|
|
|
533
|
-
def decode_number_format(format, value, name): # noqa:
|
|
534
|
-
"""Parse a custom date format string and return a formatted number value"""
|
|
545
|
+
def decode_number_format(format, value, name): # noqa: PLR0912
|
|
546
|
+
"""Parse a custom date format string and return a formatted number value."""
|
|
535
547
|
custom_format_string = format.custom_format_string
|
|
536
548
|
value *= format.scale_factor
|
|
537
549
|
if "%" in custom_format_string and format.scale_factor == 1.0:
|
|
@@ -548,6 +560,7 @@ def decode_number_format(format, value, name): # noqa: C901
|
|
|
548
560
|
warn(
|
|
549
561
|
f"Can't parse format string '{custom_format_string}'; skipping",
|
|
550
562
|
UnsupportedWarning,
|
|
563
|
+
stacklevel=1,
|
|
551
564
|
)
|
|
552
565
|
return custom_format_string
|
|
553
566
|
format_spec = match.group(1)
|
|
@@ -596,10 +609,7 @@ def decode_number_format(format, value, name): # noqa: C901
|
|
|
596
609
|
elif format.num_nonspace_integer_digits > 0:
|
|
597
610
|
int_pad = CellPadding.ZERO
|
|
598
611
|
if format.show_thousands_separator:
|
|
599
|
-
if integer != 0
|
|
600
|
-
num_commas = int(math.floor(math.log10(integer)) / 3)
|
|
601
|
-
else:
|
|
602
|
-
num_commas = 0
|
|
612
|
+
num_commas = int(math.floor(math.log10(integer)) / 3) if integer != 0 else 0
|
|
603
613
|
num_commas = max([num_commas, int((num_integers - 1) / 3)])
|
|
604
614
|
int_width = num_integers + num_commas
|
|
605
615
|
else:
|
|
@@ -653,11 +663,10 @@ def decode_number_format(format, value, name): # noqa: C901
|
|
|
653
663
|
formatted_value = f"{integer:,}".rjust(int_width)
|
|
654
664
|
else:
|
|
655
665
|
formatted_value = str(integer).rjust(int_width)
|
|
666
|
+
elif format.show_thousands_separator:
|
|
667
|
+
formatted_value = f"{integer:,}"
|
|
656
668
|
else:
|
|
657
|
-
|
|
658
|
-
formatted_value = f"{integer:,}"
|
|
659
|
-
else:
|
|
660
|
-
formatted_value = str(integer)
|
|
669
|
+
formatted_value = str(integer)
|
|
661
670
|
|
|
662
671
|
if num_decimals:
|
|
663
672
|
if dec_pad == CellPadding.ZERO or (dec_pad == CellPadding.SPACE and num_integers == 0):
|
|
@@ -683,10 +692,7 @@ def format_decimal(value: float, format) -> str:
|
|
|
683
692
|
value = -value
|
|
684
693
|
else:
|
|
685
694
|
accounting_style = False
|
|
686
|
-
if format.show_thousands_separator
|
|
687
|
-
thousands = ","
|
|
688
|
-
else:
|
|
689
|
-
thousands = ""
|
|
695
|
+
thousands = "," if format.show_thousands_separator else ""
|
|
690
696
|
|
|
691
697
|
if value.is_integer() and format.decimal_places >= DECIMAL_PLACES_AUTO:
|
|
692
698
|
formatted_value = f"{int(value):{thousands}}"
|
|
@@ -710,7 +716,7 @@ def format_decimal(value: float, format) -> str:
|
|
|
710
716
|
|
|
711
717
|
|
|
712
718
|
def float_to_fraction(value: float, denominator: int) -> str:
|
|
713
|
-
"""Convert a float to the nearest fraction and return as a string"""
|
|
719
|
+
"""Convert a float to the nearest fraction and return as a string."""
|
|
714
720
|
whole = int(value)
|
|
715
721
|
numerator = round(denominator * (value - whole))
|
|
716
722
|
if numerator == 0:
|
|
@@ -724,7 +730,8 @@ def float_to_fraction(value: float, denominator: int) -> str:
|
|
|
724
730
|
|
|
725
731
|
def float_to_n_digit_fraction(value: float, max_digits: int) -> str:
|
|
726
732
|
"""Convert a float to a fraction of a maxinum number of digits
|
|
727
|
-
and return as a string
|
|
733
|
+
and return as a string.
|
|
734
|
+
"""
|
|
728
735
|
max_denominator = 10**max_digits - 1
|
|
729
736
|
(numerator, denominator) = (
|
|
730
737
|
Fraction.from_float(value).limit_denominator(max_denominator).as_integer_ratio()
|
numbers_parser/constants.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
2
|
from enum import IntEnum
|
|
3
|
+
|
|
4
4
|
from pendulum import datetime
|
|
5
5
|
from pkg_resources import resource_filename
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ from pkg_resources import resource_filename
|
|
|
8
8
|
DEFAULT_DOCUMENT = resource_filename(__name__, os.path.join("data", "empty.numbers"))
|
|
9
9
|
DEFAULT_COLUMN_COUNT = 8
|
|
10
10
|
DEFAULT_COLUMN_WIDTH = 98.0
|
|
11
|
-
DEFAULT_PRE_BNC_BYTES = "🤠".encode(
|
|
11
|
+
DEFAULT_PRE_BNC_BYTES = "🤠".encode() # Yes, really!
|
|
12
12
|
DEFAULT_ROW_COUNT = 12
|
|
13
13
|
DEFAULT_ROW_HEIGHT = 20.0
|
|
14
14
|
DEFAULT_NUM_HEADERS = 1
|
numbers_parser/containers.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import math
|
|
2
2
|
|
|
3
|
-
from numbers_parser.file import read_numbers_file
|
|
4
3
|
from numbers_parser.constants import PACKAGE_ID
|
|
5
|
-
from numbers_parser.
|
|
4
|
+
from numbers_parser.file import read_numbers_file
|
|
5
|
+
from numbers_parser.iwafile import IWAFile, copy_object_to_iwa_file, create_iwa_segment
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ItemsList:
|
|
@@ -11,13 +11,13 @@ class ItemsList:
|
|
|
11
11
|
self._items = [item_class(model, _) for _ in refs]
|
|
12
12
|
|
|
13
13
|
def __getitem__(self, key: int):
|
|
14
|
-
if
|
|
14
|
+
if isinstance(key, int):
|
|
15
15
|
if key < 0:
|
|
16
16
|
key += len(self._items)
|
|
17
17
|
if key >= len(self._items):
|
|
18
18
|
raise IndexError(f"index {key} out of range")
|
|
19
19
|
return self._items[key]
|
|
20
|
-
elif
|
|
20
|
+
elif isinstance(key, str):
|
|
21
21
|
for item in self._items:
|
|
22
22
|
if item.name == key:
|
|
23
23
|
return item
|
|
@@ -54,7 +54,7 @@ class ObjectStore:
|
|
|
54
54
|
self._max_id = math.ceil(self._max_id / 1000000) * 1000000
|
|
55
55
|
|
|
56
56
|
def new_message_id(self):
|
|
57
|
-
"""Return the next available message ID for object creation"""
|
|
57
|
+
"""Return the next available message ID for object creation."""
|
|
58
58
|
self._max_id += 1
|
|
59
59
|
self._objects[PACKAGE_ID].last_object_identifier = self._max_id
|
|
60
60
|
return self._max_id
|
|
@@ -62,12 +62,10 @@ class ObjectStore:
|
|
|
62
62
|
def create_object_from_dict(self, iwa_file: str, object_dict: dict, cls: object):
|
|
63
63
|
"""Create a new object and store the associated IWA segment. Return the
|
|
64
64
|
message ID for the object and the newly created object. If the IWA
|
|
65
|
-
file cannot be found, it will be created.
|
|
65
|
+
file cannot be found, it will be created.
|
|
66
|
+
"""
|
|
66
67
|
paths = [k for k, v in self._file_store.items() if iwa_file in k]
|
|
67
|
-
if len(paths) == 0
|
|
68
|
-
iwa_pathname = None
|
|
69
|
-
else:
|
|
70
|
-
iwa_pathname = paths[0]
|
|
68
|
+
iwa_pathname = None if len(paths) == 0 else paths[0]
|
|
71
69
|
|
|
72
70
|
new_id = self.new_message_id()
|
|
73
71
|
iwa_segment = create_iwa_segment(new_id, cls, object_dict)
|
|
@@ -85,8 +83,9 @@ class ObjectStore:
|
|
|
85
83
|
|
|
86
84
|
def update_object_file_store(self):
|
|
87
85
|
"""Copy the protobuf messages from any updated object to the cached
|
|
88
|
-
version in the file store so this can be saved to a new document
|
|
89
|
-
|
|
86
|
+
version in the file store so this can be saved to a new document.
|
|
87
|
+
"""
|
|
88
|
+
for obj_id in self._objects:
|
|
90
89
|
copy_object_to_iwa_file(
|
|
91
90
|
self._file_store[self._object_to_filename_map[obj_id]],
|
|
92
91
|
self._objects[obj_id],
|
|
@@ -112,5 +111,4 @@ class ObjectStore:
|
|
|
112
111
|
|
|
113
112
|
# Don't cache: new tables and sheets can be added at runtime
|
|
114
113
|
def find_refs(self, ref_name) -> list:
|
|
115
|
-
|
|
116
|
-
return refs
|
|
114
|
+
return [k for k, v in self._objects.items() if type(v).__name__ == ref_name]
|