numbers-parser 4.8.0__py3-none-any.whl → 4.9.0__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/cell.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import re
2
2
  from collections import namedtuple
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  from datetime import datetime as builtin_datetime
5
5
  from datetime import timedelta as builtin_timedelta
6
6
  from enum import IntEnum
7
+ from os.path import basename
7
8
  from typing import Any, List, Tuple, Union
8
9
  from warnings import warn
9
10
 
@@ -27,6 +28,7 @@ from numbers_parser.constants import (
27
28
  EMPTY_STORAGE_BUFFER,
28
29
  MAX_BASE,
29
30
  MAX_SIGNIFICANT_DIGITS,
31
+ ControlFormattingType,
30
32
  CustomFormattingType,
31
33
  FormattingType,
32
34
  FormatType,
@@ -75,18 +77,43 @@ __all__ = [
75
77
 
76
78
 
77
79
  class BackgroundImage:
78
- def __init__(self, image_data: bytes = None, filename: str = None):
79
- self._data = image_data
80
- self._filename = filename
80
+ """
81
+ A named document style that can be applied to cells.
82
+
83
+ .. code-block:: python
84
+
85
+ fh = open("cats.png", mode="rb")
86
+ image_data = fh.read()
87
+ cats_bg = doc.add_style(
88
+ name="Cats",
89
+ bg_image=BackgroundImage(image_data, "cats.png")
90
+ )
91
+ table.write(0, 0, "❤️ cats", style=cats_bg)
92
+
93
+ Currently only standard image files and not 'advanced' image fills are
94
+ supported. Tiling and scaling is not reported back and cannot be changed
95
+ when saving new cells.
96
+
97
+ Parameters
98
+ ----------
99
+ data: bytes
100
+ Raw image data for a cell background image.
101
+ filename: str
102
+ Path to the image file.
103
+ """
104
+
105
+ def __init__(self, data: bytes = None, filename: str = None):
106
+ self._data = data
107
+ self._filename = basename(filename)
81
108
 
82
109
  @property
83
110
  def data(self) -> bytes:
84
- """The background image as byts for a cell, or None if no image."""
111
+ """bytes: The background image as bytes for a cell, or None if no image."""
85
112
  return self._data
86
113
 
87
114
  @property
88
115
  def filename(self) -> str:
89
- """The image filename for a cell, or None if no image."""
116
+ """str: The image filename for a cell, or None if no image."""
90
117
  return self._filename
91
118
 
92
119
 
@@ -184,6 +211,8 @@ class Style:
184
211
  ------
185
212
  TypeError:
186
213
  If arguments do not match the specified type or for objects have invalid arguments
214
+ IndexError:
215
+ If an image filename already exists in document
187
216
  """
188
217
 
189
218
  alignment: Alignment = DEFAULT_ALIGNMENT_CLASS # : horizontal and vertical alignment
@@ -230,6 +259,7 @@ class Style:
230
259
  return [
231
260
  "alignment",
232
261
  "bg_color",
262
+ "bg_image",
233
263
  "first_indent",
234
264
  "left_indent",
235
265
  "right_indent",
@@ -273,9 +303,9 @@ class Style:
273
303
  if not isinstance(self.font_name, str):
274
304
  raise TypeError("font name must be a string")
275
305
 
276
- for field in ["bold", "italic", "underline", "strikethrough"]:
277
- if not isinstance(getattr(self, field), bool):
278
- raise TypeError(f"{field} argument must be boolean")
306
+ for attr in ["bold", "italic", "underline", "strikethrough"]:
307
+ if not isinstance(getattr(self, attr), bool):
308
+ raise TypeError(f"{attr} argument must be boolean")
279
309
 
280
310
  def __setattr__(self, name: str, value: Any) -> None:
281
311
  """Detect changes to cell styles and flag the style for
@@ -333,6 +363,36 @@ class BorderType(IntEnum):
333
363
 
334
364
 
335
365
  class Border:
366
+ """
367
+ Create a cell border to use with the :py:class:`~numbers_parser.Table` method
368
+ :py:meth:`~numbers_parser.Table.set_cell_border`.
369
+
370
+ .. code-block:: python
371
+
372
+ border_style = Border(8.0, RGB(29, 177, 0)
373
+ table.set_cell_border("B6", "left", border_style, "solid"), 3)
374
+ table.set_cell_border(6, 1, "right", border_style, "dashes"))
375
+
376
+ Parameters
377
+ ----------
378
+ width: float, optional, default: 0.35
379
+ Number of rows in the first table of a new document.
380
+ color: RGB, optional, default: RGB(0, 0, 0)
381
+ The line color for the border if present
382
+ style: BorderType, optional, default: ``None``
383
+ The type of border to create or ``None`` if there is no border defined. Valid
384
+ border types are:
385
+
386
+ * ``"solid"``: a solid line
387
+ * ``"dashes"``: a dashed line
388
+ * ``"dots"``: a dotted line
389
+
390
+ Raises
391
+ ------
392
+ TypeError:
393
+ If the width is not a float, or the border type is invalid.
394
+ """
395
+
336
396
  def __init__(
337
397
  self,
338
398
  width: float = DEFAULT_BORDER_WIDTH,
@@ -545,8 +605,14 @@ class Cell(Cacheable):
545
605
  else:
546
606
  raise ValueError("Can't determine cell type from type " + type(value).__name__)
547
607
 
548
- def _set_formatting(self, format_id: int, format_type: FormattingType) -> None:
549
- self._storage._set_formatting(format_id, format_type)
608
+ def _set_formatting(
609
+ self,
610
+ format_id: int,
611
+ format_type: FormattingType,
612
+ control_id: int = None,
613
+ is_currency: bool = False,
614
+ ) -> None:
615
+ self._storage.set_formatting(format_id, format_type, control_id, is_currency)
550
616
 
551
617
  def __init__(self, row: int, col: int, value):
552
618
  self._value = value
@@ -675,7 +741,32 @@ class Cell(Cacheable):
675
741
 
676
742
  @property
677
743
  def formatted_value(self) -> str:
678
- """str: The formatted value of the cell as it appears in Numbers."""
744
+ """
745
+ str: The formatted value of the cell as it appears in Numbers.
746
+
747
+ Interactive elements are converted into a suitable text format where
748
+ supported, or as their number values where there is no suitable
749
+ visual representation. Currently supported mappings are:
750
+
751
+ * Checkboxes are U+2610 (Ballow Box) or U+2611 (Ballot Box with Check)
752
+ * Ratings are their star value represented using (U+2605) (Black Star)
753
+
754
+ .. code-block:: python
755
+
756
+ >>> table = doc.sheets[0].tables[0]
757
+ >>> table.cell(0,0).value
758
+ False
759
+ >>> table.cell(0,0).formatted_value
760
+ '☐'
761
+ >>> table.cell(0,1).value
762
+ True
763
+ >>> table.cell(0,1).formatted_value
764
+ '☑'
765
+ >>> table.cell(1,1).value
766
+ 3.0
767
+ >>> table.cell(1,1).formatted_value
768
+ '★★★'
769
+ """
679
770
  if self._storage is None:
680
771
  return ""
681
772
  else:
@@ -1049,16 +1140,22 @@ def xl_col_to_name(col, col_abs=False):
1049
1140
 
1050
1141
  @dataclass()
1051
1142
  class Formatting:
1052
- type: FormattingType = FormattingType.NUMBER
1143
+ allow_none: bool = False
1053
1144
  base_places: int = 0
1054
1145
  base_use_minus_sign: bool = True
1055
1146
  base: int = 10
1147
+ control_format: ControlFormattingType = ControlFormattingType.NUMBER
1056
1148
  currency_code: str = "GBP"
1057
1149
  date_time_format: str = DEFAULT_DATETIME_FORMAT
1058
1150
  decimal_places: int = None
1059
1151
  fraction_accuracy: FractionAccuracy = FractionAccuracy.THREE
1152
+ increment: float = 1.0
1153
+ maximum: float = 100.0
1154
+ minimum: float = 1.0
1155
+ popup_values: List[str] = field(default_factory=lambda: ["Item 1"])
1060
1156
  negative_style: NegativeNumberStyle = NegativeNumberStyle.MINUS
1061
1157
  show_thousands_separator: bool = False
1158
+ type: FormattingType = FormattingType.NUMBER
1062
1159
  use_accounting_style: bool = False
1063
1160
  _format_id = None
1064
1161
 
@@ -11,6 +11,8 @@ from pendulum import datetime, duration
11
11
 
12
12
  from numbers_parser import __name__ as numbers_parser_name
13
13
  from numbers_parser.constants import (
14
+ CHECKBOX_FALSE_VALUE,
15
+ CHECKBOX_TRUE_VALUE,
14
16
  CURRENCY_CELL_TYPE,
15
17
  CUSTOM_TEXT_PLACEHOLDER,
16
18
  DATETIME_FIELD_MAP,
@@ -21,6 +23,7 @@ from numbers_parser.constants import (
21
23
  SECONDS_IN_DAY,
22
24
  SECONDS_IN_HOUR,
23
25
  SECONDS_IN_WEEK,
26
+ STAR_RATING_VALUE,
24
27
  CellPadding,
25
28
  CellType,
26
29
  CustomFormattingType,
@@ -60,7 +63,7 @@ class CellStorage(Cacheable):
60
63
  # "cond_style_id",
61
64
  # "cond_rule_style_id",
62
65
  "formula_id",
63
- # "control_id",
66
+ "control_id",
64
67
  "formula_error_id",
65
68
  "suggest_id",
66
69
  "num_format_id",
@@ -95,7 +98,7 @@ class CellStorage(Cacheable):
95
98
  # self.cond_style_id = None
96
99
  # self.cond_rule_style_id = None
97
100
  self.formula_id = None
98
- # self.control_id = None
101
+ self.control_id = None
99
102
  self.formula_error_id = None
100
103
  self.suggest_id = None
101
104
  self.num_format_id = None
@@ -148,9 +151,9 @@ class CellStorage(Cacheable):
148
151
  if flags & 0x200:
149
152
  self.formula_id = unpack("<i", buffer[offset : offset + 4])[0]
150
153
  offset += 4
151
- # if flags & 0x400:
152
- # self.control_id = unpack("<i", buffer[offset : offset + 4])[0]
153
- # offset += 4
154
+ if flags & 0x400:
155
+ self.control_id = unpack("<i", buffer[offset : offset + 4])[0]
156
+ offset += 4
154
157
  # if flags & 0x800:
155
158
  # self.formula_error_id = unpack("<i", buffer[offset : offset + 4])[0]
156
159
  # offset += 4
@@ -158,7 +161,7 @@ class CellStorage(Cacheable):
158
161
  self.suggest_id = unpack("<i", buffer[offset : offset + 4])[0]
159
162
  offset += 4
160
163
  # Skip unused flags
161
- offset += 4 * bin(flags & 0xD00).count("1")
164
+ offset += 4 * bin(flags & 0x900).count("1")
162
165
  #
163
166
  if flags & 0x2000:
164
167
  self.num_format_id = unpack("<i", buffer[offset : offset + 4])[0]
@@ -300,13 +303,15 @@ class CellStorage(Cacheable):
300
303
  format = self.model.table_format(self.table_id, self.text_format_id)
301
304
  elif self.currency_format_id is not None:
302
305
  format = self.model.table_format(self.table_id, self.currency_format_id)
306
+ elif self.bool_format_id is not None and self.type == CellType.BOOL:
307
+ format = self.model.table_format(self.table_id, self.bool_format_id)
303
308
  elif self.num_format_id is not None:
304
309
  format = self.model.table_format(self.table_id, self.num_format_id)
305
- elif self.bool_format_id is not None:
306
- format = self.model.table_format(self.table_id, self.bool_format_id)
307
310
  else:
308
311
  return str(self.value)
309
312
 
313
+ debug("custom_format: @[%d,%d]: format_type=%s, ", self.row, self.col, format.format_type)
314
+
310
315
  if format.HasField("custom_uid"):
311
316
  format_uuid = NumbersUUID(format.custom_uid).hex
312
317
  format_map = self.model.custom_format_map()
@@ -336,6 +341,10 @@ class CellStorage(Cacheable):
336
341
  return format_fraction(self.d128, format)
337
342
  elif format.format_type == FormatType.SCIENTIFIC:
338
343
  return format_scientific(self.d128, format)
344
+ elif format.format_type == FormatType.CHECKBOX:
345
+ return CHECKBOX_TRUE_VALUE if self.value else CHECKBOX_FALSE_VALUE
346
+ elif format.format_type == FormatType.RATING:
347
+ return STAR_RATING_VALUE * int(self.d128)
339
348
  else:
340
349
  formatted_value = str(self.value)
341
350
  return formatted_value
@@ -362,6 +371,14 @@ class CellStorage(Cacheable):
362
371
 
363
372
  def duration_format(self) -> str:
364
373
  format = self.model.table_format(self.table_id, self.duration_format_id)
374
+ debug(
375
+ "duration_format: @[%d,%d]: table_id=%d, duration_format_id=%d, duration_style=%s",
376
+ self.row,
377
+ self.col,
378
+ self.table_id,
379
+ self.duration_format_id,
380
+ format.duration_style,
381
+ )
365
382
 
366
383
  duration_style = format.duration_style
367
384
  unit_largest = format.duration_unit_largest
@@ -431,15 +448,34 @@ class CellStorage(Cacheable):
431
448
 
432
449
  return duration_str
433
450
 
434
- def _set_formatting(
435
- self, format_id: int, format_type: Union[FormattingType, CustomFormattingType]
451
+ def set_formatting(
452
+ self,
453
+ format_id: int,
454
+ format_type: Union[FormattingType, CustomFormattingType],
455
+ control_id: int = None,
456
+ is_currency: bool = False,
436
457
  ) -> None:
458
+ self.is_currency = is_currency
437
459
  if format_type == FormattingType.CURRENCY:
438
460
  self.currency_format_id = format_id
439
- self.is_currency = True
461
+ elif format_type == FormattingType.TICKBOX:
462
+ self.bool_format_id = format_id
463
+ self.control_id = control_id
464
+ elif format_type == FormattingType.RATING:
465
+ self.num_format_id = format_id
466
+ self.control_id = control_id
467
+ elif format_type in [FormattingType.SLIDER, FormattingType.STEPPER]:
468
+ if is_currency:
469
+ self.currency_format_id = format_id
470
+ else:
471
+ self.num_format_id = format_id
472
+ self.control_id = control_id
473
+ elif format_type == FormattingType.POPUP:
474
+ self.text_format_id = format_id
475
+ self.control_id = control_id
440
476
  elif format_type in [FormattingType.DATETIME, CustomFormattingType.DATETIME]:
441
477
  self.date_format_id = format_id
442
- elif format_type == CustomFormattingType.TEXT:
478
+ elif format_type in [FormattingType.TEXT, CustomFormattingType.TEXT]:
443
479
  self.text_format_id = format_id
444
480
  else:
445
481
  self.num_format_id = format_id
@@ -20,6 +20,7 @@ __all__ = [
20
20
  "FormattingType",
21
21
  "NegativeNumberStyle",
22
22
  "FractionAccuracy",
23
+ "ControlFormattingType",
23
24
  ]
24
25
 
25
26
  DEFAULT_DOCUMENT = files("numbers_parser") / "data" / "empty.numbers"
@@ -44,8 +45,11 @@ DEFAULT_TEXT_INSET = 4.0
44
45
  DEFAULT_TEXT_WRAP = True
45
46
  EMPTY_STORAGE_BUFFER = b"\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
46
47
 
47
- # Formatting defaults
48
+ # Formatting values and defaults
48
49
  DEFAULT_DATETIME_FORMAT = "dd MMM YYY HH:MM"
50
+ CHECKBOX_FALSE_VALUE = "☐"
51
+ CHECKBOX_TRUE_VALUE = "☑"
52
+ STAR_RATING_VALUE = "★"
49
53
 
50
54
  # Numbers limits
51
55
  MAX_TILE_SIZE = 256
@@ -179,6 +183,39 @@ class FormattingType(IntEnum):
179
183
  NUMBER = 5
180
184
  PERCENTAGE = 6
181
185
  SCIENTIFIC = 7
186
+ TICKBOX = 8
187
+ RATING = 9
188
+ SLIDER = 10
189
+ STEPPER = 11
190
+ POPUP = 12
191
+ TEXT = 13
192
+
193
+
194
+ class ControlFormattingType(IntEnum):
195
+ BASE = 1
196
+ CURRENCY = 2
197
+ FRACTION = 4
198
+ NUMBER = 5
199
+ PERCENTAGE = 6
200
+ SCIENTIFIC = 7
201
+
202
+
203
+ FORMATTING_ALLOWED_CELLS = {
204
+ "base": ["NumberCell"],
205
+ "currency": ["NumberCell"],
206
+ "datetime": ["DateCell"],
207
+ "fraction": ["NumberCell"],
208
+ "number": ["NumberCell"],
209
+ "percentage": ["NumberCell"],
210
+ "popup": ["NumberCell", "TextCell"],
211
+ "rating": ["NumberCell"],
212
+ "scientific": ["NumberCell"],
213
+ "slider": ["NumberCell"],
214
+ "stepper": ["NumberCell"],
215
+ "tickbox": ["BoolCell"],
216
+ }
217
+
218
+ FORMATTING_ACTION_CELLS = ["tickbox", "rating", "popup", "slider", "stepper"]
182
219
 
183
220
 
184
221
  class CustomFormattingType(IntEnum):
@@ -187,6 +224,13 @@ class CustomFormattingType(IntEnum):
187
224
  TEXT = 103
188
225
 
189
226
 
227
+ CUSTOM_FORMATTING_ALLOWED_CELLS = {
228
+ "number": ["NumberCell"],
229
+ "datetime": ["DateCell"],
230
+ "text": ["TextCell"],
231
+ }
232
+
233
+
190
234
  @enum_tools.documentation.document_enum
191
235
  class NegativeNumberStyle(IntEnum):
192
236
  """
@@ -236,19 +280,47 @@ class FractionAccuracy(IntEnum):
236
280
 
237
281
 
238
282
  ALLOWED_FORMATTING_PARAMETERS = {
239
- FormattingType.BASE: ["base", "base_places", "base_use_minus_sign"],
283
+ FormattingType.BASE: [
284
+ "base",
285
+ "base_places",
286
+ "base_use_minus_sign",
287
+ ],
240
288
  FormattingType.CURRENCY: [
289
+ "currency_code",
241
290
  "decimal_places",
242
- "show_thousands_separator",
243
291
  "negative_style",
292
+ "show_thousands_separator",
244
293
  "use_accounting_style",
245
- "currency_code",
246
294
  ],
247
295
  FormattingType.DATETIME: ["date_time_format"],
248
296
  FormattingType.FRACTION: ["fraction_accuracy"],
249
- FormattingType.NUMBER: ["decimal_places", "show_thousands_separator", "negative_style"],
250
- FormattingType.PERCENTAGE: ["decimal_places", "show_thousands_separator", "negative_style"],
297
+ FormattingType.NUMBER: [
298
+ "decimal_places",
299
+ "show_thousands_separator",
300
+ "negative_style",
301
+ ],
302
+ FormattingType.PERCENTAGE: [
303
+ "decimal_places",
304
+ "show_thousands_separator",
305
+ "negative_style",
306
+ ],
251
307
  FormattingType.SCIENTIFIC: ["decimal_places"],
308
+ FormattingType.POPUP: ["popup_values", "allow_none"],
309
+ FormattingType.RATING: [],
310
+ FormattingType.SLIDER: [
311
+ "control_format",
312
+ "increment",
313
+ "maximum",
314
+ "minimum",
315
+ ],
316
+ FormattingType.STEPPER: [
317
+ "control_format",
318
+ "increment",
319
+ "maximum",
320
+ "minimum",
321
+ ],
322
+ FormattingType.TICKBOX: [],
323
+ FormattingType.TEXT: [],
252
324
  }
253
325
 
254
326
  FORMAT_TYPE_MAP = {
@@ -258,7 +330,13 @@ FORMAT_TYPE_MAP = {
258
330
  FormattingType.FRACTION: FormatType.FRACTION,
259
331
  FormattingType.NUMBER: FormatType.DECIMAL,
260
332
  FormattingType.PERCENTAGE: FormatType.PERCENT,
333
+ FormattingType.POPUP: FormatType.TEXT,
334
+ FormattingType.RATING: FormatType.RATING,
261
335
  FormattingType.SCIENTIFIC: FormatType.SCIENTIFIC,
336
+ FormattingType.SLIDER: FormatType.DECIMAL,
337
+ FormattingType.STEPPER: FormatType.DECIMAL,
338
+ FormattingType.TICKBOX: FormatType.CHECKBOX,
339
+ FormattingType.TEXT: FormatType.TEXT,
262
340
  }
263
341
 
264
342
  CUSTOM_FORMAT_TYPE_MAP = {
@@ -268,7 +346,32 @@ CUSTOM_FORMAT_TYPE_MAP = {
268
346
  }
269
347
 
270
348
 
349
+ class CellInteractionType(IntEnum):
350
+ VALUE_EDITING = 0
351
+ FORMULA_EDITING = 1
352
+ STOCK = 2
353
+ CATEGORY_SUMMARY = 3
354
+ STEPPER = 4
355
+ SLIDER = 5
356
+ RATING = 6
357
+ POPUP = 7
358
+ TOGGLE = 8
359
+
360
+
361
+ CONTROL_CELL_TYPE_MAP = {
362
+ FormattingType.POPUP: CellInteractionType.POPUP,
363
+ FormattingType.SLIDER: CellInteractionType.SLIDER,
364
+ FormattingType.STEPPER: CellInteractionType.STEPPER,
365
+ }
366
+
367
+
368
+ @enum_tools.documentation.document_enum
271
369
  class PaddingType(IntEnum):
370
+ """How integers and decimals are padded in custom number formats"""
371
+
272
372
  NONE = 0
373
+ """No number padding."""
273
374
  ZEROS = 1
375
+ """Pad integers with leading spaces and decimals with trailing spaces."""
274
376
  SPACES = 2
377
+ """Pad integers with leading zeroes and decimals with trailing zeroes."""
@@ -59,7 +59,7 @@ class ObjectStore:
59
59
  self._objects[PACKAGE_ID].last_object_identifier = self._max_id
60
60
  return self._max_id
61
61
 
62
- def create_object_from_dict(self, iwa_file: str, object_dict: dict, cls: object):
62
+ def create_object_from_dict(self, iwa_file: str, object_dict: dict, cls: object, append=False):
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
65
  file cannot be found, it will be created.
@@ -70,7 +70,7 @@ class ObjectStore:
70
70
  new_id = self.new_message_id()
71
71
  iwa_segment = create_iwa_segment(new_id, cls, object_dict)
72
72
 
73
- if iwa_pathname is None:
73
+ if iwa_pathname is None and not append:
74
74
  iwa_pathname = iwa_file.format(new_id) + ".iwa"
75
75
  chunks = {"chunks": [{"archives": [iwa_segment.to_dict()]}]}
76
76
  self._file_store[iwa_pathname] = IWAFile.from_dict(chunks)