numbers-parser 4.13.3__py3-none-any.whl → 4.14.2__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 +5 -4
- numbers_parser/_cat_numbers.py +24 -16
- numbers_parser/_csv2numbers.py +13 -14
- numbers_parser/_unpack_numbers.py +6 -7
- numbers_parser/bullets.py +7 -8
- numbers_parser/cell.py +280 -255
- numbers_parser/constants.py +22 -8
- numbers_parser/containers.py +11 -10
- numbers_parser/document.py +196 -150
- numbers_parser/exceptions.py +1 -8
- numbers_parser/formula.py +29 -32
- numbers_parser/generated/TSKArchives_pb2.py +92 -92
- numbers_parser/generated/TSSArchives_pb2.py +36 -36
- numbers_parser/generated/TSWPCommandArchives_pb2.py +99 -99
- numbers_parser/generated/fontmap.py +16 -10
- numbers_parser/generated/mapping.py +0 -1
- numbers_parser/iwafile.py +16 -16
- numbers_parser/iwork.py +32 -17
- numbers_parser/model.py +222 -210
- numbers_parser/numbers_cache.py +6 -7
- numbers_parser/numbers_uuid.py +4 -1
- numbers_parser/roman.py +32 -0
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/METADATA +18 -22
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/RECORD +27 -26
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/WHEEL +1 -1
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/entry_points.txt +0 -0
numbers_parser/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""Parse and extract data from Apple Numbers spreadsheets."""
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import os
|
|
@@ -20,11 +20,11 @@ _DEFAULT_NUMBERS_INSTALL_PATH = "/Applications/Numbers.app"
|
|
|
20
20
|
_VERSION_PLIST_PATH = "Contents/version.plist"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _get_version():
|
|
23
|
+
def _get_version() -> str:
|
|
24
24
|
return __version__
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def _check_installed_numbers_version():
|
|
27
|
+
def _check_installed_numbers_version() -> str:
|
|
28
28
|
try:
|
|
29
29
|
fp = open(os.path.join(_DEFAULT_NUMBERS_INSTALL_PATH, _VERSION_PLIST_PATH), "rb")
|
|
30
30
|
except OSError:
|
|
@@ -37,7 +37,8 @@ def _check_installed_numbers_version():
|
|
|
37
37
|
installed_version = re.sub(r"(\d+)\.(\d+)\.\d+", r"\1.\2", installed_version)
|
|
38
38
|
if installed_version not in SUPPORTED_NUMBERS_VERSIONS:
|
|
39
39
|
warnings.warn(
|
|
40
|
-
f"Numbers version {installed_version} not tested with this version",
|
|
40
|
+
f"Numbers version {installed_version} not tested with this version",
|
|
41
|
+
stacklevel=2,
|
|
41
42
|
)
|
|
42
43
|
fp.close()
|
|
43
44
|
return installed_version
|
numbers_parser/_cat_numbers.py
CHANGED
|
@@ -3,7 +3,7 @@ import csv
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
|
-
import sigfig
|
|
6
|
+
from sigfig import round as sigfig
|
|
7
7
|
|
|
8
8
|
from numbers_parser import Document, ErrorCell, FileError, FileFormatError, NumberCell, _get_version
|
|
9
9
|
from numbers_parser import __name__ as numbers_parser_name
|
|
@@ -49,25 +49,34 @@ def command_line_parser():
|
|
|
49
49
|
help="Dump formatted cells (durations) as they appear in Numbers",
|
|
50
50
|
)
|
|
51
51
|
parser.add_argument(
|
|
52
|
-
"-s",
|
|
52
|
+
"-s",
|
|
53
|
+
"--sheet",
|
|
54
|
+
action="append",
|
|
55
|
+
help="Names of sheet(s) to include in export",
|
|
53
56
|
)
|
|
54
57
|
parser.add_argument(
|
|
55
|
-
"-t",
|
|
58
|
+
"-t",
|
|
59
|
+
"--table",
|
|
60
|
+
action="append",
|
|
61
|
+
help="Names of table(s) to include in export",
|
|
56
62
|
)
|
|
57
63
|
parser.add_argument("document", nargs="*", help="Document(s) to export")
|
|
58
64
|
parser.add_argument("--debug", default=False, action="store_true", help="Enable debug logging")
|
|
59
65
|
parser.add_argument(
|
|
60
|
-
"--experimental",
|
|
66
|
+
"--experimental",
|
|
67
|
+
default=False,
|
|
68
|
+
action="store_true",
|
|
69
|
+
help=argparse.SUPPRESS,
|
|
61
70
|
)
|
|
62
71
|
return parser
|
|
63
72
|
|
|
64
73
|
|
|
65
|
-
def print_sheet_names(filename):
|
|
74
|
+
def print_sheet_names(filename) -> None:
|
|
66
75
|
for sheet in Document(filename).sheets:
|
|
67
76
|
print(f"{filename}: {sheet.name}")
|
|
68
77
|
|
|
69
78
|
|
|
70
|
-
def print_table_names(filename):
|
|
79
|
+
def print_table_names(filename) -> None:
|
|
71
80
|
for sheet in Document(filename).sheets:
|
|
72
81
|
for table in sheet.tables:
|
|
73
82
|
print(f"{filename}: {sheet.name}: {table.name}")
|
|
@@ -76,19 +85,18 @@ def print_table_names(filename):
|
|
|
76
85
|
def cell_as_string(args, cell):
|
|
77
86
|
if isinstance(cell, ErrorCell) and not (args.formulas):
|
|
78
87
|
return "#REF!"
|
|
79
|
-
|
|
88
|
+
if args.formulas and cell.formula is not None:
|
|
80
89
|
return cell.formula
|
|
81
|
-
|
|
90
|
+
if args.formatting and cell.formatted_value is not None:
|
|
82
91
|
return cell.formatted_value
|
|
83
|
-
|
|
84
|
-
return sigfig
|
|
85
|
-
|
|
92
|
+
if isinstance(cell, NumberCell):
|
|
93
|
+
return sigfig(cell.value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
|
|
94
|
+
if cell.value is None:
|
|
86
95
|
return ""
|
|
87
|
-
|
|
88
|
-
return str(cell.value)
|
|
96
|
+
return str(cell.value)
|
|
89
97
|
|
|
90
98
|
|
|
91
|
-
def print_table(args, filename):
|
|
99
|
+
def print_table(args, filename) -> None:
|
|
92
100
|
writer = csv.writer(sys.stdout, dialect="excel")
|
|
93
101
|
for sheet in Document(filename).sheets:
|
|
94
102
|
if args.sheet is not None and sheet.name not in args.sheet:
|
|
@@ -103,7 +111,7 @@ def print_table(args, filename):
|
|
|
103
111
|
writer.writerow(cells)
|
|
104
112
|
|
|
105
113
|
|
|
106
|
-
def main():
|
|
114
|
+
def main() -> None:
|
|
107
115
|
parser = command_line_parser()
|
|
108
116
|
args = parser.parse_args()
|
|
109
117
|
|
|
@@ -129,7 +137,7 @@ def main():
|
|
|
129
137
|
print_table_names(filename)
|
|
130
138
|
else:
|
|
131
139
|
print_table(args, filename)
|
|
132
|
-
except FileFormatError as e:
|
|
140
|
+
except FileFormatError as e: # noqa: PERF203
|
|
133
141
|
print(f"{filename}:", str(e), file=sys.stderr)
|
|
134
142
|
sys.exit(1)
|
|
135
143
|
except FileError as e:
|
numbers_parser/_csv2numbers.py
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import contextlib
|
|
6
7
|
import csv
|
|
7
8
|
import re
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from sys import exit, stderr
|
|
12
|
-
from typing import NamedTuple
|
|
13
|
+
from typing import NamedTuple
|
|
13
14
|
|
|
14
15
|
from dateutil.parser import parse
|
|
15
16
|
|
|
@@ -68,18 +69,18 @@ class Converter:
|
|
|
68
69
|
"""Parse a date string and return a datetime."""
|
|
69
70
|
return parse(x, dayfirst=self.day_first).replace(tzinfo=timezone.utc)
|
|
70
71
|
|
|
71
|
-
def _transform_data(self):
|
|
72
|
+
def _transform_data(self) -> None:
|
|
72
73
|
"""Apply type transformations to the data based in current configuration."""
|
|
73
74
|
# Convert data rows to dicts. csv.DictReader is not enough as we support CSV
|
|
74
75
|
# files with no header.
|
|
75
76
|
if self.no_header:
|
|
76
|
-
self.header =
|
|
77
|
-
self.data = [
|
|
77
|
+
self.header = list(range(len(self.data[0])))
|
|
78
|
+
self.data = [dict(dict(zip(self.header, row)).items()) for row in self.data]
|
|
78
79
|
|
|
79
80
|
if self.reverse:
|
|
80
81
|
self.data = list(reversed(self.data))
|
|
81
82
|
if self.date_columns is not None:
|
|
82
|
-
is_date_column = {x:
|
|
83
|
+
is_date_column = {x: x in self.date_columns for x in self.header}
|
|
83
84
|
for row in self.data:
|
|
84
85
|
for k, v in row.items():
|
|
85
86
|
if self.whitespace:
|
|
@@ -88,17 +89,15 @@ class Converter:
|
|
|
88
89
|
row[k] = self._parse_date(v)
|
|
89
90
|
else:
|
|
90
91
|
# Attempt to coerce value into float
|
|
91
|
-
|
|
92
|
+
with contextlib.suppress(ValueError):
|
|
92
93
|
row[k] = float(v.replace(",", ""))
|
|
93
|
-
except ValueError:
|
|
94
|
-
pass
|
|
95
94
|
|
|
96
95
|
def rename_columns(self: Converter, mapper: dict) -> None:
|
|
97
96
|
"""Rename columns using column map."""
|
|
98
97
|
if mapper is None:
|
|
99
98
|
return
|
|
100
99
|
self.no_header = False
|
|
101
|
-
self.header = [mapper
|
|
100
|
+
self.header = [mapper.get(x, x) for x in self.header]
|
|
102
101
|
|
|
103
102
|
def delete_columns(self: Converter, columns: list) -> None:
|
|
104
103
|
"""Delete columns from the data."""
|
|
@@ -131,10 +130,7 @@ class Converter:
|
|
|
131
130
|
doc = Document(num_rows=2, num_cols=2)
|
|
132
131
|
table = doc.sheets[0].tables[0]
|
|
133
132
|
|
|
134
|
-
if self.no_header
|
|
135
|
-
data = []
|
|
136
|
-
else:
|
|
137
|
-
data = [self.header]
|
|
133
|
+
data = [] if self.no_header else [self.header]
|
|
138
134
|
data += [row.values() for row in self.data]
|
|
139
135
|
|
|
140
136
|
for row_num, row in enumerate(data):
|
|
@@ -142,7 +138,10 @@ class Converter:
|
|
|
142
138
|
table.write(row_num, col_num, value)
|
|
143
139
|
if isinstance(value, datetime):
|
|
144
140
|
table.set_cell_formatting(
|
|
145
|
-
row_num,
|
|
141
|
+
row_num,
|
|
142
|
+
col_num,
|
|
143
|
+
"datetime",
|
|
144
|
+
date_time_format="d MMM yyyy",
|
|
146
145
|
)
|
|
147
146
|
|
|
148
147
|
doc.save(self.output_filename)
|
|
@@ -11,7 +11,6 @@ from binascii import hexlify
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
import regex
|
|
15
14
|
from compact_json import Formatter
|
|
16
15
|
|
|
17
16
|
from numbers_parser import __name__ as numbers_parser_name
|
|
@@ -35,7 +34,7 @@ class NumbersUnpacker(IWorkHandler):
|
|
|
35
34
|
|
|
36
35
|
def store_file(self, filename: str, blob: bytes) -> None:
|
|
37
36
|
"""Store a profobuf archive."""
|
|
38
|
-
filename =
|
|
37
|
+
filename = re.sub(r".*\.numbers/", "", str(filename))
|
|
39
38
|
self.ensure_directory_exists(filename)
|
|
40
39
|
target_path = os.path.join(self.output_dir, filename)
|
|
41
40
|
if isinstance(blob, IWAFile):
|
|
@@ -65,13 +64,13 @@ class NumbersUnpacker(IWorkHandler):
|
|
|
65
64
|
with open(target_path, "wb") as out:
|
|
66
65
|
out.write(blob)
|
|
67
66
|
|
|
68
|
-
def ensure_directory_exists(self, path: str):
|
|
67
|
+
def ensure_directory_exists(self, path: str) -> None:
|
|
69
68
|
"""Ensure that a path's directory exists."""
|
|
70
69
|
parts = os.path.split(path)
|
|
71
70
|
with contextlib.suppress(OSError):
|
|
72
71
|
os.makedirs(os.path.join(*([self.output_dir, *list(parts[:-1])])))
|
|
73
72
|
|
|
74
|
-
def prettify_uuids(self, obj: object):
|
|
73
|
+
def prettify_uuids(self, obj: object) -> None:
|
|
75
74
|
if isinstance(obj, dict):
|
|
76
75
|
for k, v in obj.items():
|
|
77
76
|
if isinstance(v, dict):
|
|
@@ -91,7 +90,7 @@ class NumbersUnpacker(IWorkHandler):
|
|
|
91
90
|
elif isinstance(v, list):
|
|
92
91
|
self.prettify_uuids(v)
|
|
93
92
|
|
|
94
|
-
def prettify_cell_storage(self, obj: object):
|
|
93
|
+
def prettify_cell_storage(self, obj: object) -> None:
|
|
95
94
|
if isinstance(obj, dict):
|
|
96
95
|
for k, v in obj.items():
|
|
97
96
|
if isinstance(v, (dict, list)):
|
|
@@ -102,7 +101,7 @@ class NumbersUnpacker(IWorkHandler):
|
|
|
102
101
|
elif k in ["cell_offsets", k == "cell_offsets_pre_bnc"]:
|
|
103
102
|
offsets = array("h", b64decode(obj[k])).tolist()
|
|
104
103
|
obj[k] = ",".join([str(x) for x in offsets])
|
|
105
|
-
obj[k] =
|
|
104
|
+
obj[k] = re.sub(r"(?:,-1)+$", ",[...]", obj[k])
|
|
106
105
|
else: # list
|
|
107
106
|
for v in obj:
|
|
108
107
|
if isinstance(v, (dict, list)):
|
|
@@ -118,7 +117,7 @@ class NumbersUnpacker(IWorkHandler):
|
|
|
118
117
|
return version in SUPPORTED_NUMBERS_VERSIONS
|
|
119
118
|
|
|
120
119
|
|
|
121
|
-
def main():
|
|
120
|
+
def main() -> None:
|
|
122
121
|
parser = ArgumentParser()
|
|
123
122
|
parser.add_argument("document", help="Apple Numbers file(s)", nargs="*")
|
|
124
123
|
parser.add_argument("-V", "--version", action="store_true")
|
numbers_parser/bullets.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
from roman import toRoman
|
|
2
|
-
|
|
3
1
|
from numbers_parser.generated.TSWPArchives_pb2 import ListStyleArchive
|
|
2
|
+
from numbers_parser.roman import to_roman
|
|
4
3
|
|
|
5
4
|
BULLET_PREFIXES = {
|
|
6
5
|
ListStyleArchive.kNumericDecimal: "",
|
|
@@ -24,12 +23,12 @@ BULLET_CONVERSION = {
|
|
|
24
23
|
ListStyleArchive.kNumericDecimal: lambda x: str(x + 1),
|
|
25
24
|
ListStyleArchive.kNumericDoubleParen: lambda x: str(x + 1),
|
|
26
25
|
ListStyleArchive.kNumericRightParen: lambda x: str(x + 1),
|
|
27
|
-
ListStyleArchive.kRomanUpperDecimal: lambda x:
|
|
28
|
-
ListStyleArchive.kRomanUpperDoubleParen: lambda x:
|
|
29
|
-
ListStyleArchive.kRomanUpperRightParen: lambda x:
|
|
30
|
-
ListStyleArchive.kRomanLowerDecimal: lambda x:
|
|
31
|
-
ListStyleArchive.kRomanLowerDoubleParen: lambda x:
|
|
32
|
-
ListStyleArchive.kRomanLowerRightParen: lambda x:
|
|
26
|
+
ListStyleArchive.kRomanUpperDecimal: lambda x: to_roman(x + 1),
|
|
27
|
+
ListStyleArchive.kRomanUpperDoubleParen: lambda x: to_roman(x + 1),
|
|
28
|
+
ListStyleArchive.kRomanUpperRightParen: lambda x: to_roman(x + 1),
|
|
29
|
+
ListStyleArchive.kRomanLowerDecimal: lambda x: to_roman(x + 1).lower(),
|
|
30
|
+
ListStyleArchive.kRomanLowerDoubleParen: lambda x: to_roman(x + 1).lower(),
|
|
31
|
+
ListStyleArchive.kRomanLowerRightParen: lambda x: to_roman(x + 1).lower(),
|
|
33
32
|
ListStyleArchive.kAlphaUpperDecimal: lambda x: chr(x + 65),
|
|
34
33
|
ListStyleArchive.kAlphaUpperDoubleParen: lambda x: chr(x + 65),
|
|
35
34
|
ListStyleArchive.kAlphaUpperRightParen: lambda x: chr(x + 65),
|