Add xlsxwriter-based Excel generation scripts with openpyxl implementation
- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py - Uses openpyxl exclusively to preserve Excel formatting and formulas - Updated server.js to use new xlsxwriter scripts for form submissions - Maintains all original functionality while ensuring proper Excel file handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
17
venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py
Normal file
17
venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
|
||||
from .cell import (
|
||||
absolute_coordinate,
|
||||
cols_from_range,
|
||||
column_index_from_string,
|
||||
coordinate_to_tuple,
|
||||
get_column_letter,
|
||||
get_column_interval,
|
||||
quote_sheetname,
|
||||
range_boundaries,
|
||||
range_to_tuple,
|
||||
rows_from_range,
|
||||
)
|
||||
|
||||
from .formulas import FORMULAE
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class BoundDictionary(defaultdict):
|
||||
"""
|
||||
A default dictionary where elements are tightly coupled.
|
||||
|
||||
The factory method is responsible for binding the parent object to the child.
|
||||
|
||||
If a reference attribute is assigned then child objects will have the key assigned to this.
|
||||
|
||||
Otherwise it's just a defaultdict.
|
||||
"""
|
||||
|
||||
def __init__(self, reference=None, *args, **kw):
|
||||
self.reference = reference
|
||||
super().__init__(*args, **kw)
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = super().__getitem__(key)
|
||||
if self.reference is not None:
|
||||
setattr(value, self.reference, key)
|
||||
return value
|
||||
240
venv/lib/python3.12/site-packages/openpyxl/utils/cell.py
Normal file
240
venv/lib/python3.12/site-packages/openpyxl/utils/cell.py
Normal file
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
"""
|
||||
Collection of utilities used within the package and also available for client code
|
||||
"""
|
||||
from functools import lru_cache
|
||||
from itertools import chain, product
|
||||
from string import ascii_uppercase, digits
|
||||
import re
|
||||
|
||||
from .exceptions import CellCoordinatesException
|
||||
|
||||
# constants
|
||||
COORD_RE = re.compile(r'^[$]?([A-Za-z]{1,3})[$]?(\d+)$')
|
||||
COL_RANGE = """[A-Z]{1,3}:[A-Z]{1,3}:"""
|
||||
ROW_RANGE = r"""\d+:\d+:"""
|
||||
RANGE_EXPR = r"""
|
||||
[$]?(?P<min_col>[A-Za-z]{1,3})?
|
||||
[$]?(?P<min_row>\d+)?
|
||||
(:[$]?(?P<max_col>[A-Za-z]{1,3})?
|
||||
[$]?(?P<max_row>\d+)?)?
|
||||
"""
|
||||
ABSOLUTE_RE = re.compile('^' + RANGE_EXPR +'$', re.VERBOSE)
|
||||
SHEET_TITLE = r"""
|
||||
(('(?P<quoted>([^']|'')*)')|(?P<notquoted>[^'^ ^!]*))!"""
|
||||
SHEETRANGE_RE = re.compile("""{0}(?P<cells>{1})(?=,?)""".format(
|
||||
SHEET_TITLE, RANGE_EXPR), re.VERBOSE)
|
||||
|
||||
|
||||
def get_column_interval(start, end):
|
||||
"""
|
||||
Given the start and end columns, return all the columns in the series.
|
||||
|
||||
The start and end columns can be either column letters or 1-based
|
||||
indexes.
|
||||
"""
|
||||
if isinstance(start, str):
|
||||
start = column_index_from_string(start)
|
||||
if isinstance(end, str):
|
||||
end = column_index_from_string(end)
|
||||
return [get_column_letter(x) for x in range(start, end + 1)]
|
||||
|
||||
|
||||
def coordinate_from_string(coord_string):
|
||||
"""Convert a coordinate string like 'B12' to a tuple ('B', 12)"""
|
||||
match = COORD_RE.match(coord_string)
|
||||
if not match:
|
||||
msg = f"Invalid cell coordinates ({coord_string})"
|
||||
raise CellCoordinatesException(msg)
|
||||
column, row = match.groups()
|
||||
row = int(row)
|
||||
if not row:
|
||||
msg = f"There is no row 0 ({coord_string})"
|
||||
raise CellCoordinatesException(msg)
|
||||
return column, row
|
||||
|
||||
|
||||
def absolute_coordinate(coord_string):
|
||||
"""Convert a coordinate to an absolute coordinate string (B12 -> $B$12)"""
|
||||
m = ABSOLUTE_RE.match(coord_string)
|
||||
if not m:
|
||||
raise ValueError(f"{coord_string} is not a valid coordinate range")
|
||||
|
||||
d = m.groupdict('')
|
||||
for k, v in d.items():
|
||||
if v:
|
||||
d[k] = f"${v}"
|
||||
|
||||
if d['max_col'] or d['max_row']:
|
||||
fmt = "{min_col}{min_row}:{max_col}{max_row}"
|
||||
else:
|
||||
fmt = "{min_col}{min_row}"
|
||||
return fmt.format(**d)
|
||||
|
||||
|
||||
__decimal_to_alpha = [""] + list(ascii_uppercase)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_column_letter(col_idx):
|
||||
"""
|
||||
Convert decimal column position to its ASCII (base 26) form.
|
||||
|
||||
Because column indices are 1-based, strides are actually pow(26, n) + 26
|
||||
Hence, a correction is applied between pow(26, n) and pow(26, 2) + 26 to
|
||||
prevent and additional column letter being prepended
|
||||
|
||||
"A" == 1 == pow(26, 0)
|
||||
"Z" == 26 == pow(26, 0) + 26 // decimal equivalent 10
|
||||
"AA" == 27 == pow(26, 1) + 1
|
||||
"ZZ" == 702 == pow(26, 2) + 26 // decimal equivalent 100
|
||||
"""
|
||||
|
||||
if not 1 <= col_idx <= 18278:
|
||||
raise ValueError("Invalid column index {0}".format(col_idx))
|
||||
|
||||
result = []
|
||||
|
||||
if col_idx < 26:
|
||||
return __decimal_to_alpha[col_idx]
|
||||
|
||||
while col_idx:
|
||||
col_idx, remainder = divmod(col_idx, 26)
|
||||
result.insert(0, __decimal_to_alpha[remainder])
|
||||
if not remainder:
|
||||
col_idx -= 1
|
||||
result.insert(0, "Z")
|
||||
|
||||
return "".join(result)
|
||||
|
||||
|
||||
__alpha_to_decimal = {letter:pos for pos, letter in enumerate(ascii_uppercase, 1)}
|
||||
__powers = (1, 26, 676)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def column_index_from_string(col):
|
||||
"""
|
||||
Convert ASCII column name (base 26) to decimal with 1-based index
|
||||
|
||||
Characters represent descending multiples of powers of 26
|
||||
|
||||
"AFZ" == 26 * pow(26, 0) + 6 * pow(26, 1) + 1 * pow(26, 2)
|
||||
"""
|
||||
error_msg = f"'{col}' is not a valid column name. Column names are from A to ZZZ"
|
||||
if len(col) > 3:
|
||||
raise ValueError(error_msg)
|
||||
idx = 0
|
||||
col = reversed(col.upper())
|
||||
for letter, power in zip(col, __powers):
|
||||
try:
|
||||
pos = __alpha_to_decimal[letter]
|
||||
except KeyError:
|
||||
raise ValueError(error_msg)
|
||||
idx += pos * power
|
||||
if not 0 < idx < 18279:
|
||||
raise ValueError(error_msg)
|
||||
return idx
|
||||
|
||||
|
||||
def range_boundaries(range_string):
|
||||
"""
|
||||
Convert a range string into a tuple of boundaries:
|
||||
(min_col, min_row, max_col, max_row)
|
||||
Cell coordinates will be converted into a range with the cell at both end
|
||||
"""
|
||||
msg = "{0} is not a valid coordinate or range".format(range_string)
|
||||
m = ABSOLUTE_RE.match(range_string)
|
||||
if not m:
|
||||
raise ValueError(msg)
|
||||
|
||||
min_col, min_row, sep, max_col, max_row = m.groups()
|
||||
|
||||
if sep:
|
||||
cols = min_col, max_col
|
||||
rows = min_row, max_row
|
||||
|
||||
if not (
|
||||
all(cols + rows) or
|
||||
all(cols) and not any(rows) or
|
||||
all(rows) and not any(cols)
|
||||
):
|
||||
raise ValueError(msg)
|
||||
|
||||
if min_col is not None:
|
||||
min_col = column_index_from_string(min_col)
|
||||
|
||||
if min_row is not None:
|
||||
min_row = int(min_row)
|
||||
|
||||
if max_col is not None:
|
||||
max_col = column_index_from_string(max_col)
|
||||
else:
|
||||
max_col = min_col
|
||||
|
||||
if max_row is not None:
|
||||
max_row = int(max_row)
|
||||
else:
|
||||
max_row = min_row
|
||||
|
||||
return min_col, min_row, max_col, max_row
|
||||
|
||||
|
||||
def rows_from_range(range_string):
|
||||
"""
|
||||
Get individual addresses for every cell in a range.
|
||||
Yields one row at a time.
|
||||
"""
|
||||
min_col, min_row, max_col, max_row = range_boundaries(range_string)
|
||||
rows = range(min_row, max_row + 1)
|
||||
cols = [get_column_letter(col) for col in range(min_col, max_col + 1)]
|
||||
for row in rows:
|
||||
yield tuple('{0}{1}'.format(col, row) for col in cols)
|
||||
|
||||
|
||||
def cols_from_range(range_string):
|
||||
"""
|
||||
Get individual addresses for every cell in a range.
|
||||
Yields one row at a time.
|
||||
"""
|
||||
min_col, min_row, max_col, max_row = range_boundaries(range_string)
|
||||
rows = range(min_row, max_row+1)
|
||||
cols = (get_column_letter(col) for col in range(min_col, max_col+1))
|
||||
for col in cols:
|
||||
yield tuple('{0}{1}'.format(col, row) for row in rows)
|
||||
|
||||
|
||||
def coordinate_to_tuple(coordinate):
|
||||
"""
|
||||
Convert an Excel style coordinate to (row, column) tuple
|
||||
"""
|
||||
for idx, c in enumerate(coordinate):
|
||||
if c in digits:
|
||||
break
|
||||
col = coordinate[:idx]
|
||||
row = coordinate[idx:]
|
||||
return int(row), column_index_from_string(col)
|
||||
|
||||
|
||||
def range_to_tuple(range_string):
|
||||
"""
|
||||
Convert a worksheet range to the sheetname and maximum and minimum
|
||||
coordinate indices
|
||||
"""
|
||||
m = SHEETRANGE_RE.match(range_string)
|
||||
if m is None:
|
||||
raise ValueError("Value must be of the form sheetname!A1:E4")
|
||||
sheetname = m.group("quoted") or m.group("notquoted")
|
||||
cells = m.group("cells")
|
||||
boundaries = range_boundaries(cells)
|
||||
return sheetname, boundaries
|
||||
|
||||
|
||||
def quote_sheetname(sheetname):
|
||||
"""
|
||||
Add quotes around sheetnames if they contain spaces.
|
||||
"""
|
||||
if "'" in sheetname:
|
||||
sheetname = sheetname.replace("'", "''")
|
||||
|
||||
sheetname = u"'{0}'".format(sheetname)
|
||||
return sheetname
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
from itertools import accumulate
|
||||
import operator
|
||||
import numpy
|
||||
from openpyxl.compat.product import prod
|
||||
|
||||
|
||||
def dataframe_to_rows(df, index=True, header=True):
|
||||
"""
|
||||
Convert a Pandas dataframe into something suitable for passing into a worksheet.
|
||||
If index is True then the index will be included, starting one row below the header.
|
||||
If header is True then column headers will be included starting one column to the right.
|
||||
Formatting should be done by client code.
|
||||
"""
|
||||
from pandas import Timestamp
|
||||
|
||||
if header:
|
||||
if df.columns.nlevels > 1:
|
||||
rows = expand_index(df.columns, header)
|
||||
else:
|
||||
rows = [list(df.columns.values)]
|
||||
for row in rows:
|
||||
n = []
|
||||
for v in row:
|
||||
if isinstance(v, numpy.datetime64):
|
||||
v = Timestamp(v)
|
||||
n.append(v)
|
||||
row = n
|
||||
if index:
|
||||
row = [None]*df.index.nlevels + row
|
||||
yield row
|
||||
|
||||
if index:
|
||||
yield df.index.names
|
||||
|
||||
expanded = ([v] for v in df.index)
|
||||
if df.index.nlevels > 1:
|
||||
expanded = expand_index(df.index)
|
||||
|
||||
# Using the expanded index is preferable to df.itertuples(index=True) so that we have 'None' inserted where applicable
|
||||
for (df_index, row) in zip(expanded, df.itertuples(index=False)):
|
||||
row = list(row)
|
||||
if index:
|
||||
row = df_index + row
|
||||
yield row
|
||||
|
||||
|
||||
def expand_index(index, header=False):
|
||||
"""
|
||||
Expand axis or column Multiindex
|
||||
For columns use header = True
|
||||
For axes use header = False (default)
|
||||
"""
|
||||
|
||||
# For each element of the index, zip the members with the previous row
|
||||
# If the 2 elements of the zipped list do not match, we can insert the new value into the row
|
||||
# or if an earlier member was different, all later members should be added to the row
|
||||
values = list(index.values)
|
||||
previous_value = [None] * len(values[0])
|
||||
result = []
|
||||
|
||||
for value in values:
|
||||
row = [None] * len(value)
|
||||
|
||||
# Once there's a difference in member of an index with the prior index, we need to store all subsequent members in the row
|
||||
prior_change = False
|
||||
for idx, (current_index_member, previous_index_member) in enumerate(zip(value, previous_value)):
|
||||
|
||||
if current_index_member != previous_index_member or prior_change:
|
||||
row[idx] = current_index_member
|
||||
prior_change = True
|
||||
|
||||
previous_value = value
|
||||
|
||||
# If this is for a row index, we're already returning a row so just yield
|
||||
if not header:
|
||||
yield row
|
||||
else:
|
||||
result.append(row)
|
||||
|
||||
# If it's for a header, we need to transpose to get it in row order
|
||||
# Example: result = [['A', 'A'], [None, 'B']] -> [['A', None], ['A', 'B']]
|
||||
if header:
|
||||
result = numpy.array(result).transpose().tolist()
|
||||
for row in result:
|
||||
yield row
|
||||
140
venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py
Normal file
140
venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
"""Manage Excel date weirdness."""
|
||||
|
||||
# Python stdlib imports
|
||||
import datetime
|
||||
from math import isnan
|
||||
import re
|
||||
|
||||
|
||||
# constants
|
||||
MAC_EPOCH = datetime.datetime(1904, 1, 1)
|
||||
WINDOWS_EPOCH = datetime.datetime(1899, 12, 30)
|
||||
CALENDAR_WINDOWS_1900 = 2415018.5 # Julian date of WINDOWS_EPOCH
|
||||
CALENDAR_MAC_1904 = 2416480.5 # Julian date of MAC_EPOCH
|
||||
CALENDAR_WINDOWS_1900 = WINDOWS_EPOCH
|
||||
CALENDAR_MAC_1904 = MAC_EPOCH
|
||||
SECS_PER_DAY = 86400
|
||||
|
||||
ISO_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
|
||||
ISO_REGEX = re.compile(r'''
|
||||
(?P<date>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}))?T?
|
||||
(?P<time>(?P<hour>\d{2}):(?P<minute>\d{2})(:(?P<second>\d{2})(?P<microsecond>\.\d{1,3})?)?)?Z?''',
|
||||
re.VERBOSE)
|
||||
ISO_DURATION = re.compile(r'PT((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+(\.\d{1,3})?)S)?')
|
||||
|
||||
|
||||
def to_ISO8601(dt):
|
||||
"""Convert from a datetime to a timestamp string."""
|
||||
if hasattr(dt, "microsecond") and dt.microsecond:
|
||||
return dt.isoformat(timespec="milliseconds")
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
def from_ISO8601(formatted_string):
|
||||
"""Convert from a timestamp string to a datetime object. According to
|
||||
18.17.4 in the specification the following ISO 8601 formats are
|
||||
supported.
|
||||
|
||||
Dates B.1.1 and B.2.1
|
||||
Times B.1.2 and B.2.2
|
||||
Datetimes B.1.3 and B.2.3
|
||||
|
||||
There is no concept of timedeltas in the specification, but Excel
|
||||
writes them (in strict OOXML mode), so these are also understood.
|
||||
"""
|
||||
if not formatted_string:
|
||||
return None
|
||||
|
||||
match = ISO_REGEX.match(formatted_string)
|
||||
if match and any(match.groups()):
|
||||
parts = match.groupdict(0)
|
||||
for key in ["year", "month", "day", "hour", "minute", "second"]:
|
||||
if parts[key]:
|
||||
parts[key] = int(parts[key])
|
||||
|
||||
if parts["microsecond"]:
|
||||
parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)
|
||||
|
||||
if not parts["date"]:
|
||||
dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
|
||||
elif not parts["time"]:
|
||||
dt = datetime.date(parts['year'], parts['month'], parts['day'])
|
||||
else:
|
||||
del parts["time"]
|
||||
del parts["date"]
|
||||
dt = datetime.datetime(**parts)
|
||||
return dt
|
||||
|
||||
match = ISO_DURATION.match(formatted_string)
|
||||
if match and any(match.groups()):
|
||||
parts = match.groupdict(0)
|
||||
for key, val in parts.items():
|
||||
if val:
|
||||
parts[key] = float(val)
|
||||
return datetime.timedelta(**parts)
|
||||
|
||||
raise ValueError("Invalid datetime value {}".format(formatted_string))
|
||||
|
||||
|
||||
def to_excel(dt, epoch=WINDOWS_EPOCH):
|
||||
"""Convert Python datetime to Excel serial"""
|
||||
if isinstance(dt, datetime.time):
|
||||
return time_to_days(dt)
|
||||
if isinstance(dt, datetime.timedelta):
|
||||
return timedelta_to_days(dt)
|
||||
if isnan(dt.year): # Pandas supports Not a Date
|
||||
return
|
||||
|
||||
if not hasattr(dt, "date"):
|
||||
dt = datetime.datetime.combine(dt, datetime.time())
|
||||
|
||||
# rebase on epoch and adjust for < 1900-03-01
|
||||
days = (dt - epoch).days
|
||||
if 0 < days <= 60 and epoch == WINDOWS_EPOCH:
|
||||
days -= 1
|
||||
return days + time_to_days(dt)
|
||||
|
||||
|
||||
def from_excel(value, epoch=WINDOWS_EPOCH, timedelta=False):
|
||||
"""Convert Excel serial to Python datetime"""
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if timedelta:
|
||||
td = datetime.timedelta(days=value)
|
||||
if td.microseconds:
|
||||
# round to millisecond precision
|
||||
td = datetime.timedelta(seconds=td.total_seconds() // 1,
|
||||
microseconds=round(td.microseconds, -3))
|
||||
return td
|
||||
|
||||
day, fraction = divmod(value, 1)
|
||||
diff = datetime.timedelta(milliseconds=round(fraction * SECS_PER_DAY * 1000))
|
||||
if 0 <= value < 1 and diff.days == 0:
|
||||
return days_to_time(diff)
|
||||
if 0 < value < 60 and epoch == WINDOWS_EPOCH:
|
||||
day += 1
|
||||
return epoch + datetime.timedelta(days=day) + diff
|
||||
|
||||
|
||||
def time_to_days(value):
|
||||
"""Convert a time value to fractions of day"""
|
||||
return (
|
||||
(value.hour * 3600)
|
||||
+ (value.minute * 60)
|
||||
+ value.second
|
||||
+ value.microsecond / 10**6
|
||||
) / SECS_PER_DAY
|
||||
|
||||
|
||||
def timedelta_to_days(value):
|
||||
"""Convert a timedelta value to fractions of a day"""
|
||||
return value.total_seconds() / SECS_PER_DAY
|
||||
|
||||
|
||||
def days_to_time(value):
|
||||
mins, seconds = divmod(value.seconds, 60)
|
||||
hours, mins = divmod(mins, 60)
|
||||
return datetime.time(hours, mins, seconds, value.microseconds)
|
||||
43
venv/lib/python3.12/site-packages/openpyxl/utils/escape.py
Normal file
43
venv/lib/python3.12/site-packages/openpyxl/utils/escape.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
"""
|
||||
OOXML has non-standard escaping for characters < \031
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def escape(value):
|
||||
r"""
|
||||
Convert ASCII < 31 to OOXML: \n == _x + hex(ord(\n)) + _
|
||||
"""
|
||||
|
||||
CHAR_REGEX = re.compile(r"[\001-\031]")
|
||||
|
||||
def _sub(match):
|
||||
"""
|
||||
Callback to escape chars
|
||||
"""
|
||||
return "_x{:0>4x}_".format(ord(match.group(0)))
|
||||
|
||||
return CHAR_REGEX.sub(_sub, value)
|
||||
|
||||
|
||||
def unescape(value):
|
||||
r"""
|
||||
Convert escaped strings to ASCIII: _x000a_ == \n
|
||||
"""
|
||||
|
||||
|
||||
ESCAPED_REGEX = re.compile("_x([0-9A-Fa-f]{4})_")
|
||||
|
||||
def _sub(match):
|
||||
"""
|
||||
Callback to unescape chars
|
||||
"""
|
||||
return chr(int(match.group(1), 16))
|
||||
|
||||
if "_x" in value:
|
||||
value = ESCAPED_REGEX.sub(_sub, value)
|
||||
|
||||
return value
|
||||
@@ -0,0 +1,34 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
|
||||
"""Definitions for openpyxl shared exception classes."""
|
||||
|
||||
|
||||
class CellCoordinatesException(Exception):
|
||||
"""Error for converting between numeric and A1-style cell references."""
|
||||
|
||||
|
||||
class IllegalCharacterError(Exception):
|
||||
"""The data submitted which cannot be used directly in Excel files. It
|
||||
must be removed or escaped."""
|
||||
|
||||
|
||||
class NamedRangeException(Exception):
|
||||
"""Error for badly formatted named ranges."""
|
||||
|
||||
|
||||
class SheetTitleException(Exception):
|
||||
"""Error for bad sheet names."""
|
||||
|
||||
|
||||
class InvalidFileException(Exception):
|
||||
"""Error for trying to open a non-ooxml file."""
|
||||
|
||||
|
||||
class ReadOnlyWorkbookException(Exception):
|
||||
"""Error for trying to modify a read-only workbook"""
|
||||
|
||||
|
||||
class WorkbookAlreadySaved(Exception):
|
||||
"""Error when attempting to perform operations on a dump workbook
|
||||
while it has already been dumped once"""
|
||||
24
venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py
Normal file
24
venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
"""
|
||||
List of builtin formulae
|
||||
"""
|
||||
|
||||
FORMULAE = ("CUBEKPIMEMBER", "CUBEMEMBER", "CUBEMEMBERPROPERTY", "CUBERANKEDMEMBER", "CUBESET", "CUBESETCOUNT", "CUBEVALUE", "DAVERAGE", "DCOUNT", "DCOUNTA", "DGET", "DMAX", "DMIN", "DPRODUCT", "DSTDEV", "DSTDEVP", "DSUM", "DVAR", "DVARP", "DATE", "DATEDIF", "DATEVALUE", "DAY", "DAYS360", "EDATE", "EOMONTH", "HOUR", "MINUTE", "MONTH", "NETWORKDAYS", "NETWORKDAYS.INTL", "NOW", "SECOND", "TIME", "TIMEVALUE", "TODAY", "WEEKDAY", "WEEKNUM", "WORKDAY", "WORKDAY.INTL", "YEAR", "YEARFRAC", "BESSELI", "BESSELJ", "BESSELK", "BESSELY", "BIN2DEC", "BIN2HEX", "BIN2OCT", "COMPLEX", "CONVERT", "DEC2BIN", "DEC2HEX", "DEC2OCT", "DELTA", "ERF", "ERFC", "GESTEP", "HEX2BIN", "HEX2DEC", "HEX2OCT", "IMABS", "IMAGINARY", "IMARGUMENT", "IMCONJUGATE", "IMCOS", "IMDIV", "IMEXP", "IMLN", "IMLOG10", "IMLOG2", "IMPOWER", "IMPRODUCT", "IMREAL", "IMSIN", "IMSQRT", "IMSUB", "IMSUM", "OCT2BIN", "OCT2DEC", "OCT2HEX", "ACCRINT", "ACCRINTM", "AMORDEGRC", "AMORLINC", "COUPDAYBS", "COUPDAYS", "COUPDAYSNC", "COUPNCD", "COUPNUM", "COUPPCD", "CUMIPMT", "CUMPRINC", "DB", "DDB", "DISC", "DOLLARDE", "DOLLARFR", "DURATION", "EFFECT", "FV", "FVSCHEDULE", "INTRATE", "IPMT", "IRR", "ISPMT", "MDURATION", "MIRR", "NOMINAL", "NPER", "NPV", "ODDFPRICE", "ODDFYIELD", "ODDLPRICE", "ODDLYIELD", "PMT", "PPMT", "PRICE", "PRICEDISC", "PRICEMAT", "PV", "RATE", "RECEIVED", "SLN", "SYD", "TBILLEQ", "TBILLPRICE", "TBILLYIELD", "VDB", "XIRR", "XNPV", "YIELD", "YIELDDISC", "YIELDMAT", "CELL", "ERROR.TYPE", "INFO", "ISBLANK", "ISERR", "ISERROR", "ISEVEN", "ISLOGICAL", "ISNA", "ISNONTEXT", "ISNUMBER", "ISODD", "ISREF", "ISTEXT", "N", "NA", "TYPE", "AND", "FALSE", "IF", "IFERROR", "NOT", "OR", "TRUE", "ADDRESS", "AREAS", "CHOOSE", "COLUMN", "COLUMNS", "GETPIVOTDATA", "HLOOKUP", "HYPERLINK", "INDEX", "INDIRECT", "LOOKUP", "MATCH", "OFFSET", "ROW", "ROWS", "RTD", "TRANSPOSE", "VLOOKUP", "ABS", "ACOS", "ACOSH", "ASIN", "ASINH", "ATAN", "ATAN2", "ATANH", "CEILING", "COMBIN", "COS", "COSH", "DEGREES", "ECMA.CEILING", "EVEN", "EXP", "FACT", "FACTDOUBLE", "FLOOR", "GCD", "INT", "ISO.CEILING", "LCM", "LN", "LOG", "LOG10", "MDETERM", "MINVERSE", "MMULT", "MOD", "MROUND", "MULTINOMIAL", "ODD", "PI", "POWER", "PRODUCT", "QUOTIENT", "RADIANS", "RAND", "RANDBETWEEN", "ROMAN", "ROUND", "ROUNDDOWN", "ROUNDUP", "SERIESSUM", "SIGN", "SIN", "SINH", "SQRT", "SQRTPI", "SUBTOTAL", "SUM", "SUMIF", "SUMIFS", "SUMPRODUCT", "SUMSQ", "SUMX2MY2", "SUMX2PY2", "SUMXMY2", "TAN", "TANH", "TRUNC", "AVEDEV", "AVERAGE", "AVERAGEA", "AVERAGEIF", "AVERAGEIFS", "BETADIST", "BETAINV", "BINOMDIST", "CHIDIST", "CHIINV", "CHITEST", "CONFIDENCE", "CORREL", "COUNT", "COUNTA", "COUNTBLANK", "COUNTIF", "COUNTIFS", "COVAR", "CRITBINOM", "DEVSQ", "EXPONDIST", "FDIST", "FINV", "FISHER", "FISHERINV", "FORECAST", "FREQUENCY", "FTEST", "GAMMADIST", "GAMMAINV", "GAMMALN", "GEOMEAN", "GROWTH", "HARMEAN", "HYPGEOMDIST", "INTERCEPT", "KURT", "LARGE", "LINEST", "LOGEST", "LOGINV", "LOGNORMDIST", "MAX", "MAXA", "MEDIAN", "MIN", "MINA", "MODE", "NEGBINOMDIST", "NORMDIST", "NORMINV", "NORMSDIST", "NORMSINV", "PEARSON", "PERCENTILE", "PERCENTRANK", "PERMUT", "POISSON", "PROB", "QUARTILE", "RANK", "RSQ", "SKEW", "SLOPE", "SMALL", "STANDARDIZE", "STDEV", "STDEVA", "STDEVP", "STDEVPA", "STEYX", "TDIST", "TINV", "TREND", "TRIMMEAN", "TTEST", "VAR", "VARA", "VARP", "VARPA", "WEIBULL", "ZTEST", "ASC", "BAHTTEXT", "CHAR", "CLEAN", "CODE", "CONCATENATE", "DOLLAR", "EXACT", "FIND", "FINDB", "FIXED", "JIS", "LEFT", "LEFTB", "LEN", "LENB", "LOWER", "MID", "MIDB", "PHONETIC", "PROPER", "REPLACE", "REPLACEB", "REPT", "RIGHT", "RIGHTB", "SEARCH", "SEARCHB", "SUBSTITUTE", "T", "TEXT", "TRIM", "UPPER", "VALUE")
|
||||
|
||||
FORMULAE = frozenset(FORMULAE)
|
||||
|
||||
|
||||
from openpyxl.formula import Tokenizer
|
||||
|
||||
|
||||
def validate(formula):
|
||||
"""
|
||||
Utility function for checking whether a formula is syntactically correct
|
||||
"""
|
||||
assert formula.startswith("=")
|
||||
formula = Tokenizer(formula)
|
||||
for t in formula.items:
|
||||
if t.type == "FUNC" and t.subtype == "OPEN":
|
||||
if not t.value.startswith("_xlfn.") and t.value[:-1] not in FORMULAE:
|
||||
raise ValueError(f"Unknown function {t.value} in {formula.formula}. The function may need a prefix")
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
|
||||
class IndexedList(list):
|
||||
"""
|
||||
List with optimised access by value
|
||||
Based on Alex Martelli's recipe
|
||||
|
||||
http://code.activestate.com/recipes/52303-the-auxiliary-dictionary-idiom-for-sequences-with-/
|
||||
"""
|
||||
|
||||
_dict = {}
|
||||
|
||||
def __init__(self, iterable=None):
|
||||
self.clean = True
|
||||
self._dict = {}
|
||||
if iterable is not None:
|
||||
self.clean = False
|
||||
for idx, val in enumerate(iterable):
|
||||
self._dict[val] = idx
|
||||
list.append(self, val)
|
||||
|
||||
def _rebuild_dict(self):
|
||||
self._dict = {}
|
||||
idx = 0
|
||||
for value in self:
|
||||
if value not in self._dict:
|
||||
self._dict[value] = idx
|
||||
idx += 1
|
||||
self.clean = True
|
||||
|
||||
def __contains__(self, value):
|
||||
if not self.clean:
|
||||
self._rebuild_dict()
|
||||
return value in self._dict
|
||||
|
||||
def index(self, value):
|
||||
if value in self:
|
||||
return self._dict[value]
|
||||
raise ValueError
|
||||
|
||||
def append(self, value):
|
||||
if value not in self._dict:
|
||||
self._dict[value] = len(self)
|
||||
list.append(self, value)
|
||||
|
||||
def add(self, value):
|
||||
self.append(value)
|
||||
return self._dict[value]
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
"""
|
||||
Type inference functions
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from openpyxl.styles import numbers
|
||||
|
||||
PERCENT_REGEX = re.compile(r'^(?P<number>\-?[0-9]*\.?[0-9]*\s?)\%$')
|
||||
TIME_REGEX = re.compile(r"""
|
||||
^(?: # HH:MM and HH:MM:SS
|
||||
(?P<hour>[0-1]{0,1}[0-9]{2}):
|
||||
(?P<minute>[0-5][0-9]):?
|
||||
(?P<second>[0-5][0-9])?$)
|
||||
|
|
||||
^(?: # MM:SS.
|
||||
([0-5][0-9]):
|
||||
([0-5][0-9])?\.
|
||||
(?P<microsecond>\d{1,6}))
|
||||
""", re.VERBOSE)
|
||||
NUMBER_REGEX = re.compile(r'^-?([\d]|[\d]+\.[\d]*|\.[\d]+|[1-9][\d]+\.?[\d]*)((E|e)[-+]?[\d]+)?$')
|
||||
|
||||
|
||||
def cast_numeric(value):
|
||||
"""Explicitly convert a string to a numeric value"""
|
||||
if NUMBER_REGEX.match(value):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return float(value)
|
||||
|
||||
|
||||
def cast_percentage(value):
|
||||
"""Explicitly convert a string to numeric value and format as a
|
||||
percentage"""
|
||||
match = PERCENT_REGEX.match(value)
|
||||
if match:
|
||||
return float(match.group('number')) / 100
|
||||
|
||||
|
||||
|
||||
def cast_time(value):
|
||||
"""Explicitly convert a string to a number and format as datetime or
|
||||
time"""
|
||||
match = TIME_REGEX.match(value)
|
||||
if match:
|
||||
if match.group("microsecond") is not None:
|
||||
value = value[:12]
|
||||
pattern = "%M:%S.%f"
|
||||
#fmt = numbers.FORMAT_DATE_TIME5
|
||||
elif match.group('second') is None:
|
||||
#fmt = numbers.FORMAT_DATE_TIME3
|
||||
pattern = "%H:%M"
|
||||
else:
|
||||
pattern = "%H:%M:%S"
|
||||
#fmt = numbers.FORMAT_DATE_TIME6
|
||||
value = datetime.datetime.strptime(value, pattern)
|
||||
return value.time()
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
|
||||
def hash_password(plaintext_password=''):
|
||||
"""
|
||||
Create a password hash from a given string for protecting a worksheet
|
||||
only. This will not work for encrypting a workbook.
|
||||
|
||||
This method is based on the algorithm provided by
|
||||
Daniel Rentz of OpenOffice and the PEAR package
|
||||
Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
|
||||
See also http://blogs.msdn.com/b/ericwhite/archive/2008/02/23/the-legacy-hashing-algorithm-in-open-xml.aspx
|
||||
"""
|
||||
password = 0x0000
|
||||
for idx, char in enumerate(plaintext_password, 1):
|
||||
value = ord(char) << idx
|
||||
rotated_bits = value >> 15
|
||||
value &= 0x7fff
|
||||
password ^= (value | rotated_bits)
|
||||
password ^= len(plaintext_password)
|
||||
password ^= 0xCE4B
|
||||
return str(hex(password)).upper()[2:]
|
||||
108
venv/lib/python3.12/site-packages/openpyxl/utils/units.py
Normal file
108
venv/lib/python3.12/site-packages/openpyxl/utils/units.py
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
# Copyright (c) 2010-2024 openpyxl
|
||||
|
||||
import math
|
||||
|
||||
|
||||
#constants
|
||||
|
||||
DEFAULT_ROW_HEIGHT = 15. # Default row height measured in point size.
|
||||
BASE_COL_WIDTH = 8 # in characters
|
||||
DEFAULT_COLUMN_WIDTH = BASE_COL_WIDTH + 5
|
||||
# = baseColumnWidth + {margin padding (2 pixels on each side, totalling 4 pixels)} + {gridline (1pixel)}
|
||||
|
||||
|
||||
DEFAULT_LEFT_MARGIN = 0.7 # in inches, = right margin
|
||||
DEFAULT_TOP_MARGIN = 0.7874 # in inches = bottom margin
|
||||
DEFAULT_HEADER = 0.3 # in inches
|
||||
|
||||
|
||||
# Conversion functions
|
||||
"""
|
||||
From the ECMA Spec (4th Edition part 1)
|
||||
Page setup: "Left Page Margin in inches" p. 1647
|
||||
|
||||
Docs from
|
||||
http://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
|
||||
|
||||
See also http://msdn.microsoft.com/en-us/library/dd560821(v=office.12).aspx
|
||||
|
||||
dxa: The main unit in OOXML is a twentieth of a point. Also called twips.
|
||||
pt: point. In Excel there are 72 points to an inch
|
||||
hp: half-points are used to specify font sizes. A font-size of 12pt equals 24 half points
|
||||
pct: Half-points are used to specify font sizes. A font-size of 12pt equals 24 half points
|
||||
|
||||
EMU: English Metric Unit, EMUs are used for coordinates in vector-based
|
||||
drawings and embedded pictures. One inch equates to 914400 EMUs and a
|
||||
centimeter is 360000. For bitmaps the default resolution is 96 dpi (known as
|
||||
PixelsPerInch in Excel). Spec p. 1122
|
||||
|
||||
For radial geometry Excel uses integer units of 1/60000th of a degree.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def inch_to_dxa(value):
|
||||
"""1 inch = 72 * 20 dxa"""
|
||||
return int(value * 20 * 72)
|
||||
|
||||
def dxa_to_inch(value):
|
||||
return value / 72 / 20
|
||||
|
||||
|
||||
def dxa_to_cm(value):
|
||||
return 2.54 * dxa_to_inch(value)
|
||||
|
||||
def cm_to_dxa(value):
|
||||
emu = cm_to_EMU(value)
|
||||
inch = EMU_to_inch(emu)
|
||||
return inch_to_dxa(inch)
|
||||
|
||||
|
||||
def pixels_to_EMU(value):
|
||||
"""1 pixel = 9525 EMUs"""
|
||||
return int(value * 9525)
|
||||
|
||||
def EMU_to_pixels(value):
|
||||
return round(value / 9525)
|
||||
|
||||
|
||||
def cm_to_EMU(value):
|
||||
"""1 cm = 360000 EMUs"""
|
||||
return int(value * 360000)
|
||||
|
||||
def EMU_to_cm(value):
|
||||
return round(value / 360000, 4)
|
||||
|
||||
|
||||
def inch_to_EMU(value):
|
||||
"""1 inch = 914400 EMUs"""
|
||||
return int(value * 914400)
|
||||
|
||||
def EMU_to_inch(value):
|
||||
return round(value / 914400, 4)
|
||||
|
||||
|
||||
def pixels_to_points(value, dpi=96):
|
||||
"""96 dpi, 72i"""
|
||||
return value * 72 / dpi
|
||||
|
||||
|
||||
def points_to_pixels(value, dpi=96):
|
||||
return int(math.ceil(value * dpi / 72))
|
||||
|
||||
|
||||
def degrees_to_angle(value):
|
||||
"""1 degree = 60000 angles"""
|
||||
return int(round(value * 60000))
|
||||
|
||||
|
||||
def angle_to_degrees(value):
|
||||
return round(value / 60000, 2)
|
||||
|
||||
|
||||
def short_color(color):
|
||||
""" format a color to its short size """
|
||||
if len(color) > 6:
|
||||
return color[2:]
|
||||
return color
|
||||
Reference in New Issue
Block a user