aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/openpyxl/utils
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/openpyxl/utils')
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py17
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py26
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py240
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py87
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py140
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py43
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py34
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py24
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py49
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py60
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py22
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/units.py108
12 files changed, 850 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py
new file mode 100644
index 00000000..f6132636
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py
@@ -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
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py
new file mode 100644
index 00000000..20cbd1c4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py
@@ -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
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py
new file mode 100644
index 00000000..f1ccc7d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py
@@ -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
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py
new file mode 100644
index 00000000..f56a4887
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py
@@ -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
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py
new file mode 100644
index 00000000..bf7e5006
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py
@@ -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)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py
new file mode 100644
index 00000000..a8985343
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py
@@ -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
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py
new file mode 100644
index 00000000..7b05f742
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py
@@ -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"""
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py
new file mode 100644
index 00000000..aab9961b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py
@@ -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")
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py
new file mode 100644
index 00000000..753acf09
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py
@@ -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]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py
new file mode 100644
index 00000000..aff02a2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py
@@ -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()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py
new file mode 100644
index 00000000..cc7707ee
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py
@@ -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:]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/units.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/units.py
new file mode 100644
index 00000000..19f23c5b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/units.py
@@ -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