tdrpa.tdworker 1.2.13.2__py312-none-win_amd64.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.
- tdrpa/_tdxlwings/__init__.py +193 -0
- tdrpa/_tdxlwings/__pycache__/__init__.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/__init__.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/apps.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/apps.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/base_classes.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/base_classes.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/com_server.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/com_server.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/constants.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/constants.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/expansion.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/expansion.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/main.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/main.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/udfs.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/udfs.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/utils.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/utils.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/_win32patch.py +90 -0
- tdrpa/_tdxlwings/_xlmac.py +2240 -0
- tdrpa/_tdxlwings/_xlwindows.py +2518 -0
- tdrpa/_tdxlwings/addin/Dictionary.cls +474 -0
- tdrpa/_tdxlwings/addin/IWebAuthenticator.cls +71 -0
- tdrpa/_tdxlwings/addin/WebClient.cls +772 -0
- tdrpa/_tdxlwings/addin/WebHelpers.bas +3203 -0
- tdrpa/_tdxlwings/addin/WebRequest.cls +875 -0
- tdrpa/_tdxlwings/addin/WebResponse.cls +453 -0
- tdrpa/_tdxlwings/addin/xlwings.xlam +0 -0
- tdrpa/_tdxlwings/apps.py +35 -0
- tdrpa/_tdxlwings/base_classes.py +1092 -0
- tdrpa/_tdxlwings/cli.py +1306 -0
- tdrpa/_tdxlwings/com_server.py +385 -0
- tdrpa/_tdxlwings/constants.py +3080 -0
- tdrpa/_tdxlwings/conversion/__init__.py +103 -0
- tdrpa/_tdxlwings/conversion/framework.py +147 -0
- tdrpa/_tdxlwings/conversion/numpy_conv.py +34 -0
- tdrpa/_tdxlwings/conversion/pandas_conv.py +184 -0
- tdrpa/_tdxlwings/conversion/standard.py +321 -0
- tdrpa/_tdxlwings/expansion.py +83 -0
- tdrpa/_tdxlwings/ext/__init__.py +3 -0
- tdrpa/_tdxlwings/ext/sql.py +73 -0
- tdrpa/_tdxlwings/html/xlwings-alert.html +71 -0
- tdrpa/_tdxlwings/js/xlwings.js +577 -0
- tdrpa/_tdxlwings/js/xlwings.ts +729 -0
- tdrpa/_tdxlwings/mac_dict.py +6399 -0
- tdrpa/_tdxlwings/main.py +5205 -0
- tdrpa/_tdxlwings/mistune/__init__.py +63 -0
- tdrpa/_tdxlwings/mistune/block_parser.py +366 -0
- tdrpa/_tdxlwings/mistune/inline_parser.py +216 -0
- tdrpa/_tdxlwings/mistune/markdown.py +84 -0
- tdrpa/_tdxlwings/mistune/renderers.py +220 -0
- tdrpa/_tdxlwings/mistune/scanner.py +121 -0
- tdrpa/_tdxlwings/mistune/util.py +41 -0
- tdrpa/_tdxlwings/pro/__init__.py +40 -0
- tdrpa/_tdxlwings/pro/_xlcalamine.py +536 -0
- tdrpa/_tdxlwings/pro/_xlofficejs.py +146 -0
- tdrpa/_tdxlwings/pro/_xlremote.py +1293 -0
- tdrpa/_tdxlwings/pro/custom_functions_code.js +150 -0
- tdrpa/_tdxlwings/pro/embedded_code.py +60 -0
- tdrpa/_tdxlwings/pro/udfs_officejs.py +549 -0
- tdrpa/_tdxlwings/pro/utils.py +199 -0
- tdrpa/_tdxlwings/quickstart.xlsm +0 -0
- tdrpa/_tdxlwings/quickstart_addin.xlam +0 -0
- tdrpa/_tdxlwings/quickstart_addin_ribbon.xlam +0 -0
- tdrpa/_tdxlwings/quickstart_fastapi/main.py +47 -0
- tdrpa/_tdxlwings/quickstart_fastapi/requirements.txt +3 -0
- tdrpa/_tdxlwings/quickstart_standalone.xlsm +0 -0
- tdrpa/_tdxlwings/reports.py +12 -0
- tdrpa/_tdxlwings/rest/__init__.py +1 -0
- tdrpa/_tdxlwings/rest/api.py +368 -0
- tdrpa/_tdxlwings/rest/serializers.py +103 -0
- tdrpa/_tdxlwings/server.py +14 -0
- tdrpa/_tdxlwings/udfs.py +775 -0
- tdrpa/_tdxlwings/utils.py +777 -0
- tdrpa/_tdxlwings/xlwings-0.31.6.applescript +30 -0
- tdrpa/_tdxlwings/xlwings.bas +2061 -0
- tdrpa/_tdxlwings/xlwings_custom_addin.bas +2042 -0
- tdrpa/_tdxlwings/xlwingslib.cp38-win_amd64.pyd +0 -0
- tdrpa/tdworker/__init__.pyi +12 -0
- tdrpa/tdworker/_clip.pyi +50 -0
- tdrpa/tdworker/_excel.pyi +743 -0
- tdrpa/tdworker/_file.pyi +77 -0
- tdrpa/tdworker/_img.pyi +226 -0
- tdrpa/tdworker/_network.pyi +94 -0
- tdrpa/tdworker/_os.pyi +47 -0
- tdrpa/tdworker/_sp.pyi +21 -0
- tdrpa/tdworker/_w.pyi +129 -0
- tdrpa/tdworker/_web.pyi +995 -0
- tdrpa/tdworker/_winE.pyi +228 -0
- tdrpa/tdworker/_winK.pyi +74 -0
- tdrpa/tdworker/_winM.pyi +117 -0
- tdrpa/tdworker.cp312-win_amd64.pyd +0 -0
- tdrpa_tdworker-1.2.13.2.dist-info/METADATA +38 -0
- tdrpa_tdworker-1.2.13.2.dist-info/RECORD +101 -0
- tdrpa_tdworker-1.2.13.2.dist-info/WHEEL +5 -0
- tdrpa_tdworker-1.2.13.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,777 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
import os
|
3
|
+
import re
|
4
|
+
import subprocess
|
5
|
+
import sys
|
6
|
+
import tempfile
|
7
|
+
import traceback
|
8
|
+
import uuid
|
9
|
+
from functools import lru_cache, total_ordering
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
try:
|
13
|
+
import numpy as np
|
14
|
+
except ImportError:
|
15
|
+
np = None
|
16
|
+
|
17
|
+
try:
|
18
|
+
import matplotlib as mpl
|
19
|
+
import matplotlib.figure # noqa: F401
|
20
|
+
import matplotlib.pyplot as plt
|
21
|
+
|
22
|
+
# https://matplotlib.org/stable/users/explain/figure/backends.html#static-backends
|
23
|
+
# This prevents "Starting a Matplotlib GUI outside of the main thread will likely
|
24
|
+
# fail." with xlwings Server
|
25
|
+
mpl.use("agg")
|
26
|
+
except ImportError:
|
27
|
+
mpl = None
|
28
|
+
|
29
|
+
try:
|
30
|
+
import plotly.graph_objects as plotly_go
|
31
|
+
except ImportError:
|
32
|
+
plotly_go = None
|
33
|
+
|
34
|
+
import tdrpa._tdxlwings as xlwings
|
35
|
+
|
36
|
+
missing = object()
|
37
|
+
|
38
|
+
|
39
|
+
def int_to_rgb(number):
|
40
|
+
"""Given an integer, return the rgb"""
|
41
|
+
number = int(number)
|
42
|
+
r = number % 256
|
43
|
+
g = (number // 256) % 256
|
44
|
+
b = (number // (256 * 256)) % 256
|
45
|
+
return r, g, b
|
46
|
+
|
47
|
+
|
48
|
+
def rgb_to_int(rgb):
|
49
|
+
"""Given an rgb, return an int"""
|
50
|
+
return rgb[0] + (rgb[1] * 256) + (rgb[2] * 256 * 256)
|
51
|
+
|
52
|
+
|
53
|
+
def hex_to_rgb(color):
|
54
|
+
color = color[1:] if color.startswith("#") else color
|
55
|
+
return tuple(int(color[i : i + 2], 16) for i in (0, 2, 4))
|
56
|
+
|
57
|
+
|
58
|
+
def rgb_to_hex(r, g, b):
|
59
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
60
|
+
|
61
|
+
|
62
|
+
def get_duplicates(seq):
|
63
|
+
seen = set()
|
64
|
+
duplicates = set(x for x in seq if x in seen or seen.add(x))
|
65
|
+
return duplicates
|
66
|
+
|
67
|
+
|
68
|
+
def np_datetime_to_datetime(np_datetime):
|
69
|
+
ts = (np_datetime - np.datetime64("1970-01-01T00:00:00Z")) / np.timedelta64(1, "s")
|
70
|
+
dt_datetime = dt.datetime.utcfromtimestamp(ts)
|
71
|
+
return dt_datetime
|
72
|
+
|
73
|
+
|
74
|
+
def xlserial_to_datetime(serial):
|
75
|
+
"""
|
76
|
+
Converts a date in Excel's serial format (e.g., 44197.0) to a Python datetime object
|
77
|
+
"""
|
78
|
+
# https://learn.microsoft.com/en-us/office/dev/scripts/resources/samples/excel-samples#dates
|
79
|
+
ts = round((serial - 25569) * 86400, 3)
|
80
|
+
return dt.datetime.utcfromtimestamp(ts) # tz-naive, which is what we want
|
81
|
+
|
82
|
+
|
83
|
+
def datetime_to_xlserial(obj):
|
84
|
+
"""
|
85
|
+
Converts a Python date or datetime object to Excel's date serial (e.g, 44197.0)
|
86
|
+
"""
|
87
|
+
if isinstance(obj, dt.datetime):
|
88
|
+
obj = obj.replace(tzinfo=dt.timezone.utc)
|
89
|
+
elif isinstance(obj, dt.date):
|
90
|
+
obj = dt.datetime(
|
91
|
+
obj.year,
|
92
|
+
obj.month,
|
93
|
+
obj.day,
|
94
|
+
tzinfo=dt.timezone.utc,
|
95
|
+
)
|
96
|
+
return obj.timestamp() / 86400 + 25569
|
97
|
+
|
98
|
+
|
99
|
+
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
100
|
+
|
101
|
+
|
102
|
+
def col_name(i):
|
103
|
+
i -= 1
|
104
|
+
if i < 0:
|
105
|
+
raise IndexError(i)
|
106
|
+
elif i < 26:
|
107
|
+
return ALPHABET[i]
|
108
|
+
elif i < 702:
|
109
|
+
i -= 26
|
110
|
+
return ALPHABET[i // 26] + ALPHABET[i % 26]
|
111
|
+
elif i < 16384:
|
112
|
+
i -= 702
|
113
|
+
return ALPHABET[i // 676] + ALPHABET[i // 26 % 26] + ALPHABET[i % 26]
|
114
|
+
else:
|
115
|
+
raise IndexError(i)
|
116
|
+
|
117
|
+
|
118
|
+
def address_to_index_tuple(address):
|
119
|
+
"""
|
120
|
+
Based on a function from XlsxWriter, which is distributed under the following
|
121
|
+
BSD 2-Clause License:
|
122
|
+
|
123
|
+
Copyright (c) 2013-2021, John McNamara <jmcnamara@cpan.org>
|
124
|
+
All rights reserved.
|
125
|
+
|
126
|
+
Redistribution and use in source and binary forms, with or without
|
127
|
+
modification, are permitted provided that the following conditions are met:
|
128
|
+
|
129
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
130
|
+
list of conditions and the following disclaimer.
|
131
|
+
|
132
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
133
|
+
this list of conditions and the following disclaimer in the documentation
|
134
|
+
and/or other materials provided with the distribution.
|
135
|
+
|
136
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
137
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
138
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
139
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
140
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
141
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
142
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
143
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
144
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
145
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
146
|
+
"""
|
147
|
+
re_range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
|
148
|
+
match = re_range_parts.match(address)
|
149
|
+
if match:
|
150
|
+
col_str = match.group(2)
|
151
|
+
row_str = match.group(4)
|
152
|
+
|
153
|
+
# Convert base26 column string to number
|
154
|
+
expn = 0
|
155
|
+
col = 0
|
156
|
+
for char in reversed(col_str):
|
157
|
+
col += (ord(char) - ord("A") + 1) * (26**expn)
|
158
|
+
expn += 1
|
159
|
+
|
160
|
+
return int(row_str), col
|
161
|
+
|
162
|
+
|
163
|
+
def a1_to_tuples(address):
|
164
|
+
if ":" in address:
|
165
|
+
address1, address2 = address.split(":")
|
166
|
+
tuple1 = address_to_index_tuple(address1.upper())
|
167
|
+
tuple2 = address_to_index_tuple(address2.upper())
|
168
|
+
else:
|
169
|
+
tuple1 = address_to_index_tuple(address.upper())
|
170
|
+
tuple2 = None
|
171
|
+
return tuple1, tuple2
|
172
|
+
|
173
|
+
|
174
|
+
class VBAWriter:
|
175
|
+
MAX_VBA_LINE_LENGTH = 1024
|
176
|
+
VBA_LINE_SPLIT = " _\n"
|
177
|
+
MAX_VBA_SPLITTED_LINE_LENGTH = MAX_VBA_LINE_LENGTH - len(VBA_LINE_SPLIT)
|
178
|
+
|
179
|
+
class Block:
|
180
|
+
def __init__(self, writer, start):
|
181
|
+
self.writer = writer
|
182
|
+
self.start = start
|
183
|
+
|
184
|
+
def __enter__(self):
|
185
|
+
self.writer.writeln(self.start)
|
186
|
+
self.writer._indent += 1
|
187
|
+
|
188
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
189
|
+
self.writer._indent -= 1
|
190
|
+
|
191
|
+
def __init__(self, f):
|
192
|
+
self.f = f
|
193
|
+
self._indent = 0
|
194
|
+
self._freshline = True
|
195
|
+
|
196
|
+
def block(self, template, **kwargs):
|
197
|
+
return VBAWriter.Block(self, template.format(**kwargs))
|
198
|
+
|
199
|
+
def start_block(self, template, **kwargs):
|
200
|
+
self.writeln(template, **kwargs)
|
201
|
+
self._indent += 1
|
202
|
+
|
203
|
+
def end_block(self, template, **kwargs):
|
204
|
+
self.writeln(template, **kwargs)
|
205
|
+
self._indent -= 1
|
206
|
+
|
207
|
+
def write(self, template, **kwargs):
|
208
|
+
if kwargs:
|
209
|
+
template = template.format(**kwargs)
|
210
|
+
if self._freshline:
|
211
|
+
template = (" " * self._indent) + template
|
212
|
+
self._freshline = False
|
213
|
+
self.write_vba_line(template)
|
214
|
+
if template[-1] == "\n":
|
215
|
+
self._freshline = True
|
216
|
+
|
217
|
+
def write_label(self, label):
|
218
|
+
self._indent -= 1
|
219
|
+
self.write(label + ":\n")
|
220
|
+
self._indent += 1
|
221
|
+
|
222
|
+
def writeln(self, template, **kwargs):
|
223
|
+
self.write(template + "\n", **kwargs)
|
224
|
+
|
225
|
+
def write_vba_line(self, vba_line):
|
226
|
+
if len(vba_line) > VBAWriter.MAX_VBA_LINE_LENGTH:
|
227
|
+
separator_index = VBAWriter.get_separator_index(vba_line)
|
228
|
+
self.f.write(vba_line[:separator_index] + VBAWriter.VBA_LINE_SPLIT)
|
229
|
+
self.write_vba_line(vba_line[separator_index:])
|
230
|
+
else:
|
231
|
+
self.f.write(vba_line)
|
232
|
+
|
233
|
+
@classmethod
|
234
|
+
def get_separator_index(cls, vba_line):
|
235
|
+
for index in range(cls.MAX_VBA_SPLITTED_LINE_LENGTH, 0, -1):
|
236
|
+
if " " == vba_line[index]:
|
237
|
+
return index
|
238
|
+
return (
|
239
|
+
cls.MAX_VBA_SPLITTED_LINE_LENGTH
|
240
|
+
) # Best effort: split string at the maximum possible length
|
241
|
+
|
242
|
+
|
243
|
+
def try_parse_int(x):
|
244
|
+
try:
|
245
|
+
return int(x)
|
246
|
+
except ValueError:
|
247
|
+
return x
|
248
|
+
|
249
|
+
|
250
|
+
@total_ordering
|
251
|
+
class VersionNumber:
|
252
|
+
def __init__(self, s):
|
253
|
+
self.value = tuple(map(try_parse_int, s.split(".")))
|
254
|
+
|
255
|
+
@property
|
256
|
+
def major(self):
|
257
|
+
return self.value[0]
|
258
|
+
|
259
|
+
@property
|
260
|
+
def minor(self):
|
261
|
+
return self.value[1] if len(self.value) > 1 else None
|
262
|
+
|
263
|
+
def __str__(self):
|
264
|
+
return ".".join(map(str, self.value))
|
265
|
+
|
266
|
+
def __repr__(self):
|
267
|
+
return "%s(%s)" % (self.__class__.__name__, repr(str(self)))
|
268
|
+
|
269
|
+
def __eq__(self, other):
|
270
|
+
if isinstance(other, VersionNumber):
|
271
|
+
return self.value == other.value
|
272
|
+
elif isinstance(other, str):
|
273
|
+
return self.value == VersionNumber(other).value
|
274
|
+
elif isinstance(other, tuple):
|
275
|
+
return self.value[: len(other)] == other
|
276
|
+
elif isinstance(other, int):
|
277
|
+
return self.major == other
|
278
|
+
else:
|
279
|
+
return False
|
280
|
+
|
281
|
+
def __lt__(self, other):
|
282
|
+
if isinstance(other, VersionNumber):
|
283
|
+
return self.value < other.value
|
284
|
+
elif isinstance(other, str):
|
285
|
+
return self.value < VersionNumber(other).value
|
286
|
+
elif isinstance(other, tuple):
|
287
|
+
return self.value[: len(other)] < other
|
288
|
+
elif isinstance(other, int):
|
289
|
+
return self.major < other
|
290
|
+
else:
|
291
|
+
raise TypeError("Cannot compare other object with version number")
|
292
|
+
|
293
|
+
|
294
|
+
def process_image(image, format, export_options):
|
295
|
+
"""Returns filename and is_temp_file"""
|
296
|
+
image = fspath(image)
|
297
|
+
if isinstance(image, str):
|
298
|
+
return image, False
|
299
|
+
elif mpl and isinstance(image, mpl.figure.Figure):
|
300
|
+
image_type = "mpl"
|
301
|
+
elif plotly_go and isinstance(image, plotly_go.Figure):
|
302
|
+
image_type = "plotly"
|
303
|
+
else:
|
304
|
+
raise TypeError("Don't know what to do with that image object")
|
305
|
+
|
306
|
+
if export_options is None:
|
307
|
+
export_options = {"bbox_inches": "tight", "dpi": 200}
|
308
|
+
|
309
|
+
if format == "vector":
|
310
|
+
if sys.platform.startswith("darwin"):
|
311
|
+
format = "pdf"
|
312
|
+
else:
|
313
|
+
format = "svg"
|
314
|
+
|
315
|
+
temp_dir = os.path.realpath(tempfile.gettempdir())
|
316
|
+
filename = os.path.join(temp_dir, str(uuid.uuid4()) + "." + format)
|
317
|
+
|
318
|
+
if image_type == "mpl":
|
319
|
+
canvas = mpl.backends.backend_agg.FigureCanvas(image)
|
320
|
+
canvas.draw()
|
321
|
+
image.savefig(filename, **export_options)
|
322
|
+
plt.close(image)
|
323
|
+
elif image_type == "plotly":
|
324
|
+
image.write_image(filename)
|
325
|
+
return filename, True
|
326
|
+
|
327
|
+
|
328
|
+
def fspath(path):
|
329
|
+
"""Convert path-like object to string.
|
330
|
+
|
331
|
+
On python <= 3.5 the input argument is always returned unchanged (no support for
|
332
|
+
path-like objects available). TODO: can be removed as 3.5 no longer supported.
|
333
|
+
|
334
|
+
"""
|
335
|
+
if hasattr(os, "PathLike") and isinstance(path, os.PathLike):
|
336
|
+
return os.fspath(path)
|
337
|
+
else:
|
338
|
+
return path
|
339
|
+
|
340
|
+
|
341
|
+
def read_config_sheet(book):
|
342
|
+
try:
|
343
|
+
return book.sheets["xlwings.conf"]["A1:B1"].options(dict, expand="down").value
|
344
|
+
except: # noqa: E722
|
345
|
+
# A missing sheet currently produces different errors on mac and win
|
346
|
+
return {}
|
347
|
+
|
348
|
+
|
349
|
+
def read_user_config():
|
350
|
+
"""Returns keys in lowercase of xlwings.conf in the user's home directory"""
|
351
|
+
config = {}
|
352
|
+
if Path(xlwings.USER_CONFIG_FILE).is_file():
|
353
|
+
with open(xlwings.USER_CONFIG_FILE, "r") as f:
|
354
|
+
for line in f:
|
355
|
+
values = re.findall(r'"[^"]*"', line)
|
356
|
+
if values:
|
357
|
+
config[values[0].strip('"').lower()] = os.path.expandvars(
|
358
|
+
values[1].strip('"')
|
359
|
+
)
|
360
|
+
return config
|
361
|
+
|
362
|
+
|
363
|
+
@lru_cache(None)
|
364
|
+
def get_cached_user_config(key):
|
365
|
+
return read_user_config().get(key.lower())
|
366
|
+
|
367
|
+
|
368
|
+
def exception(logger, msg, *args):
|
369
|
+
if logger.hasHandlers():
|
370
|
+
logger.exception(msg, *args)
|
371
|
+
else:
|
372
|
+
print(msg % args)
|
373
|
+
traceback.print_exc()
|
374
|
+
|
375
|
+
|
376
|
+
def chunk(sequence, chunksize):
|
377
|
+
for i in range(0, len(sequence), chunksize):
|
378
|
+
yield sequence[i : i + chunksize]
|
379
|
+
|
380
|
+
|
381
|
+
def query_yes_no(question, default="yes"):
|
382
|
+
"""Ask a yes/no question via input() and return their answer.
|
383
|
+
|
384
|
+
"question" is a string that is presented to the user.
|
385
|
+
"default" is the presumed answer if the user just hits <Enter>.
|
386
|
+
It must be "yes" (the default), "no" or None (meaning
|
387
|
+
an answer is required of the user).
|
388
|
+
|
389
|
+
The "answer" return value is True for "yes" or False for "no".
|
390
|
+
|
391
|
+
Licensed under the MIT License
|
392
|
+
Copyright by Trent Mick
|
393
|
+
https://code.activestate.com/recipes/577058/
|
394
|
+
"""
|
395
|
+
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
|
396
|
+
if default is None:
|
397
|
+
prompt = " [y/n] "
|
398
|
+
elif default == "yes":
|
399
|
+
prompt = " [Y/n] "
|
400
|
+
elif default == "no":
|
401
|
+
prompt = " [y/N] "
|
402
|
+
else:
|
403
|
+
raise ValueError("invalid default answer: '%s'" % default)
|
404
|
+
|
405
|
+
while True:
|
406
|
+
sys.stdout.write(question + prompt)
|
407
|
+
choice = input().lower()
|
408
|
+
if default is not None and choice == "":
|
409
|
+
return valid[default]
|
410
|
+
elif choice in valid:
|
411
|
+
return valid[choice]
|
412
|
+
else:
|
413
|
+
sys.stdout.write("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
|
414
|
+
|
415
|
+
|
416
|
+
def prepare_sys_path(args_string):
|
417
|
+
"""Called from Excel to prepend the default paths and those from the PYTHONPATH
|
418
|
+
setting to sys.path. While RunPython could use Book.caller(), the UDF server can't,
|
419
|
+
as this runs before VBA can push the ActiveWorkbook over. UDFs also can't interact
|
420
|
+
with the book object in general as Excel is busy during the function call and so
|
421
|
+
won't allow you to read out the config sheet, for example. Before 0.24.9,
|
422
|
+
these manipulations were handled in VBA, but couldn't handle SharePoint.
|
423
|
+
"""
|
424
|
+
args = os.path.normcase(os.path.expandvars(args_string)).split(";")
|
425
|
+
paths = []
|
426
|
+
if args[0].lower() == "true": # Add dir of Excel file to PYTHONPATH
|
427
|
+
# Not sure if we really need normcase,
|
428
|
+
# but on Windows it replaces "/" with "\", so let's revert that
|
429
|
+
active_fullname = args[1].replace("\\", "/")
|
430
|
+
this_fullname = args[2].replace("\\", "/")
|
431
|
+
for fullname in [active_fullname, this_fullname]:
|
432
|
+
if not fullname:
|
433
|
+
continue
|
434
|
+
elif "://" in fullname:
|
435
|
+
fullname = Path(
|
436
|
+
fullname_url_to_local_path(
|
437
|
+
url=fullname,
|
438
|
+
sheet_onedrive_consumer_config=args[3],
|
439
|
+
sheet_onedrive_commercial_config=args[4],
|
440
|
+
sheet_sharepoint_config=args[5],
|
441
|
+
)
|
442
|
+
)
|
443
|
+
else:
|
444
|
+
fullname = Path(fullname)
|
445
|
+
paths += [str(fullname.parent), str(fullname.with_suffix(".zip"))]
|
446
|
+
|
447
|
+
if args[6:]:
|
448
|
+
paths += args[6:]
|
449
|
+
|
450
|
+
if paths:
|
451
|
+
sys.path[0:0] = list(set(paths))
|
452
|
+
|
453
|
+
|
454
|
+
@lru_cache(None)
|
455
|
+
def fullname_url_to_local_path(
|
456
|
+
url,
|
457
|
+
sheet_onedrive_consumer_config=None,
|
458
|
+
sheet_onedrive_commercial_config=None,
|
459
|
+
sheet_sharepoint_config=None,
|
460
|
+
):
|
461
|
+
"""
|
462
|
+
When AutoSave is enabled in Excel with either OneDrive or SharePoint, VBA/COM's
|
463
|
+
Workbook.FullName turns into a URL without any possibilities to get the local file
|
464
|
+
path. While OneDrive and OneDrive for Business make it easy enough to derive the
|
465
|
+
local path from the URL, SharePoint allows to define the "Site name" and "Site
|
466
|
+
address" independently from each other with the former ending up in the local folder
|
467
|
+
path and the latter in the FullName URL. Adding to the complexity: (1) When the site
|
468
|
+
name contains spaces, they will be stripped out from the URL and (2) you can sync a
|
469
|
+
subfolder directly (this, at least, works when you have a single folder at the
|
470
|
+
SharePoint's Document root), which results in skipping a folder level locally when
|
471
|
+
compared to the online/URL version. And (3) the OneDriveCommercial env var sometimes
|
472
|
+
seems to actually point to the local SharePoint folder.
|
473
|
+
|
474
|
+
Parameters
|
475
|
+
----------
|
476
|
+
url : str
|
477
|
+
URL as returned by VBA's FullName
|
478
|
+
|
479
|
+
sheet_onedrive_consumer_config : str
|
480
|
+
Optional Path to the local OneDrive (Personal) as defined in the Workbook's
|
481
|
+
config sheet
|
482
|
+
|
483
|
+
sheet_onedrive_commercial_config : str
|
484
|
+
Optional Path to the local OneDrive for Business as defined in the Workbook's
|
485
|
+
config sheet
|
486
|
+
|
487
|
+
sheet_sharepoint_config : str
|
488
|
+
Optional Path to the local SharePoint drive as defined in the Workbook's config
|
489
|
+
sheet
|
490
|
+
"""
|
491
|
+
# Directory config files can't be used
|
492
|
+
# since the whole purpose of this exercise is to find out about a book's dir
|
493
|
+
onedrive_consumer_config_name = (
|
494
|
+
"ONEDRIVE_CONSUMER_WIN"
|
495
|
+
if sys.platform.startswith("win")
|
496
|
+
else "ONEDRIVE_CONSUMER_MAC"
|
497
|
+
)
|
498
|
+
onedrive_commercial_config_name = (
|
499
|
+
"ONEDRIVE_COMMERCIAL_WIN"
|
500
|
+
if sys.platform.startswith("win")
|
501
|
+
else "ONEDRIVE_COMMERCIAL_MAC"
|
502
|
+
)
|
503
|
+
sharepoint_config_name = (
|
504
|
+
"SHAREPOINT_WIN" if sys.platform.startswith("win") else "SHAREPOINT_MAC"
|
505
|
+
)
|
506
|
+
if sheet_onedrive_consumer_config is not None:
|
507
|
+
sheet_onedrive_consumer_config = os.path.expandvars(
|
508
|
+
sheet_onedrive_consumer_config
|
509
|
+
)
|
510
|
+
if sheet_onedrive_commercial_config is not None:
|
511
|
+
sheet_onedrive_commercial_config = os.path.expandvars(
|
512
|
+
sheet_onedrive_commercial_config
|
513
|
+
)
|
514
|
+
if sheet_sharepoint_config is not None:
|
515
|
+
sheet_sharepoint_config = os.path.expandvars(sheet_sharepoint_config)
|
516
|
+
onedrive_consumer_config = sheet_onedrive_consumer_config or read_user_config().get(
|
517
|
+
onedrive_consumer_config_name.lower()
|
518
|
+
)
|
519
|
+
onedrive_commercial_config = (
|
520
|
+
sheet_onedrive_commercial_config
|
521
|
+
or read_user_config().get(onedrive_commercial_config_name.lower())
|
522
|
+
)
|
523
|
+
sharepoint_config = sheet_sharepoint_config or read_user_config().get(
|
524
|
+
sharepoint_config_name.lower()
|
525
|
+
)
|
526
|
+
|
527
|
+
# OneDrive
|
528
|
+
pattern = re.compile(r"https://d.docs.live.net/[^/]*/(.*)")
|
529
|
+
match = pattern.match(url)
|
530
|
+
if match:
|
531
|
+
if sys.platform.startswith("darwin"):
|
532
|
+
default_dir = Path.home() / "Library" / "CloudStorage" / "OneDrive-Personal"
|
533
|
+
else:
|
534
|
+
default_dir = Path.home() / "OneDrive"
|
535
|
+
legacy_default_dir = Path.home() / "OneDrive"
|
536
|
+
root = (
|
537
|
+
onedrive_consumer_config
|
538
|
+
or os.getenv("OneDriveConsumer")
|
539
|
+
or os.getenv("OneDrive")
|
540
|
+
or (str(default_dir) if default_dir.is_dir() else None)
|
541
|
+
or (str(legacy_default_dir) if legacy_default_dir else None)
|
542
|
+
)
|
543
|
+
if not root:
|
544
|
+
raise xlwings.XlwingsError(
|
545
|
+
f"Couldn't find the local OneDrive folder. Please configure the "
|
546
|
+
f"{onedrive_consumer_config_name} setting, see: xlwings.org/error."
|
547
|
+
)
|
548
|
+
local_path = Path(root) / match.group(1)
|
549
|
+
if local_path.is_file():
|
550
|
+
return str(local_path)
|
551
|
+
else:
|
552
|
+
raise xlwings.XlwingsError(
|
553
|
+
"Couldn't find your local OneDrive file, see: xlwings.org/error"
|
554
|
+
)
|
555
|
+
|
556
|
+
# OneDrive for Business
|
557
|
+
pattern = re.compile(r"https://[^-]*-my.sharepoint.[^/]*/[^/]*/[^/]*/[^/]*/(.*)")
|
558
|
+
match = pattern.match(url)
|
559
|
+
if match:
|
560
|
+
root = (
|
561
|
+
onedrive_commercial_config
|
562
|
+
or os.getenv("OneDriveCommercial")
|
563
|
+
or os.getenv("OneDrive")
|
564
|
+
)
|
565
|
+
if not root:
|
566
|
+
raise xlwings.XlwingsError(
|
567
|
+
f"Couldn't find the local OneDrive for Business folder. "
|
568
|
+
f"Please configure the {onedrive_commercial_config_name} setting, "
|
569
|
+
f"see: xlwings.org/error."
|
570
|
+
)
|
571
|
+
local_path = Path(root) / match.group(1)
|
572
|
+
if local_path.is_file():
|
573
|
+
return str(local_path)
|
574
|
+
else:
|
575
|
+
raise xlwings.XlwingsError(
|
576
|
+
"Couldn't find your local OneDrive for Business file, "
|
577
|
+
"see: xlwings.org/error"
|
578
|
+
)
|
579
|
+
# SharePoint Online & On-Premises
|
580
|
+
pattern = re.compile(r"https?://[^/]*/sites/([^/]*)/([^/]*)/(.*)")
|
581
|
+
match = pattern.match(url)
|
582
|
+
if match:
|
583
|
+
# xlwings config
|
584
|
+
if sharepoint_config:
|
585
|
+
root = sharepoint_config
|
586
|
+
local_path = Path(root) / f"{match.group(1)} - Documents" / match.group(3)
|
587
|
+
if local_path.is_file():
|
588
|
+
return str(local_path)
|
589
|
+
# Env var
|
590
|
+
if os.getenv("OneDriveCommercial"):
|
591
|
+
# Default top level mapping
|
592
|
+
root = os.getenv("OneDriveCommercial").replace("OneDrive - ", "")
|
593
|
+
local_path = Path(root) / f"{match.group(1)} - Documents" / match.group(3)
|
594
|
+
if local_path.is_file():
|
595
|
+
return str(local_path)
|
596
|
+
# Windows registry
|
597
|
+
url_to_mount = get_url_to_mount()
|
598
|
+
for url_namespace, mount_point in url_to_mount.items():
|
599
|
+
if url.startswith(url_namespace):
|
600
|
+
local_path = Path(mount_point) / url[len(url_namespace) :]
|
601
|
+
if local_path.is_file():
|
602
|
+
return str(local_path)
|
603
|
+
# Horrible fallback
|
604
|
+
return search_local_sharepoint_path(
|
605
|
+
url, mount_point, sharepoint_config, sharepoint_config_name
|
606
|
+
)
|
607
|
+
raise xlwings.XlwingsError(
|
608
|
+
f"URL {url} not recognized as valid OneDrive/SharePoint link."
|
609
|
+
)
|
610
|
+
|
611
|
+
|
612
|
+
def to_pdf(
|
613
|
+
obj,
|
614
|
+
path=None,
|
615
|
+
include=None,
|
616
|
+
exclude=None,
|
617
|
+
layout=None,
|
618
|
+
exclude_start_string=None,
|
619
|
+
show=None,
|
620
|
+
quality=None,
|
621
|
+
):
|
622
|
+
report_path = fspath(path)
|
623
|
+
layout_path = fspath(layout)
|
624
|
+
if isinstance(obj, (xlwings.Book, xlwings.Sheet)):
|
625
|
+
if report_path is None:
|
626
|
+
filename, extension = os.path.splitext(obj.fullname)
|
627
|
+
directory, _ = os.path.split(obj.fullname)
|
628
|
+
if directory:
|
629
|
+
report_path = os.path.join(directory, filename + ".pdf")
|
630
|
+
else:
|
631
|
+
report_path = filename + ".pdf"
|
632
|
+
if (include is not None) and (exclude is not None):
|
633
|
+
raise ValueError("You can only use either 'include' or 'exclude'")
|
634
|
+
# Hide sheets to exclude them from printing
|
635
|
+
if isinstance(include, (str, int)):
|
636
|
+
include = [include]
|
637
|
+
if isinstance(exclude, (str, int)):
|
638
|
+
exclude = [exclude]
|
639
|
+
exclude_by_name = [
|
640
|
+
sheet.index
|
641
|
+
for sheet in obj.sheets
|
642
|
+
if sheet.name.startswith(exclude_start_string)
|
643
|
+
]
|
644
|
+
visibility = {}
|
645
|
+
if include or exclude or exclude_by_name:
|
646
|
+
for sheet in obj.sheets:
|
647
|
+
visibility[sheet] = sheet.visible
|
648
|
+
try:
|
649
|
+
if include:
|
650
|
+
for sheet in obj.sheets:
|
651
|
+
if (sheet.name in include) or (sheet.index in include):
|
652
|
+
sheet.visible = True
|
653
|
+
else:
|
654
|
+
sheet.visible = False
|
655
|
+
if exclude or exclude_by_name:
|
656
|
+
exclude = [] if exclude is None else exclude
|
657
|
+
for sheet in obj.sheets:
|
658
|
+
if (
|
659
|
+
(sheet.name in exclude)
|
660
|
+
or (sheet.index in exclude)
|
661
|
+
or (sheet.index in exclude_by_name)
|
662
|
+
):
|
663
|
+
sheet.visible = False
|
664
|
+
obj.impl.to_pdf(os.path.realpath(report_path), quality=quality)
|
665
|
+
except Exception:
|
666
|
+
raise
|
667
|
+
finally:
|
668
|
+
# Reset visibility
|
669
|
+
if include or exclude or exclude_by_name:
|
670
|
+
for sheet, tf in visibility.items():
|
671
|
+
sheet.visible = tf
|
672
|
+
else:
|
673
|
+
if report_path is None:
|
674
|
+
if isinstance(obj, xlwings.Chart):
|
675
|
+
directory, _ = os.path.split(obj.parent.book.fullname)
|
676
|
+
filename = obj.name
|
677
|
+
elif isinstance(obj, xlwings.Range):
|
678
|
+
directory, _ = os.path.split(obj.sheet.book.fullname)
|
679
|
+
filename = (
|
680
|
+
str(obj)
|
681
|
+
.replace("<", "")
|
682
|
+
.replace(">", "")
|
683
|
+
.replace(":", "_")
|
684
|
+
.replace(" ", "")
|
685
|
+
)
|
686
|
+
else:
|
687
|
+
raise ValueError(f"Object of type {type(obj)} are not supported.")
|
688
|
+
if directory:
|
689
|
+
report_path = os.path.join(directory, filename + ".pdf")
|
690
|
+
else:
|
691
|
+
report_path = filename + ".pdf"
|
692
|
+
obj.impl.to_pdf(os.path.realpath(report_path), quality=quality)
|
693
|
+
|
694
|
+
if layout:
|
695
|
+
from .pro.reports.pdf import print_on_layout
|
696
|
+
|
697
|
+
print_on_layout(report_path=report_path, layout_path=layout_path)
|
698
|
+
|
699
|
+
if show:
|
700
|
+
if sys.platform.startswith("win"):
|
701
|
+
os.startfile(report_path)
|
702
|
+
else:
|
703
|
+
subprocess.run(["open", report_path])
|
704
|
+
return report_path
|
705
|
+
|
706
|
+
|
707
|
+
def get_url_to_mount():
|
708
|
+
"""Windows stores the sharepoint mount points in the registry. This helps but still
|
709
|
+
isn't foolproof.
|
710
|
+
"""
|
711
|
+
if sys.platform.startswith("win"):
|
712
|
+
import winreg
|
713
|
+
from winreg import HKEY_CURRENT_USER, KEY_READ
|
714
|
+
|
715
|
+
root = r"SOFTWARE\SyncEngines\Providers\OneDrive"
|
716
|
+
url_to_mount = {}
|
717
|
+
try:
|
718
|
+
with winreg.OpenKey(HKEY_CURRENT_USER, root, 0, KEY_READ) as root_key:
|
719
|
+
for i in range(0, winreg.QueryInfoKey(root_key)[0]):
|
720
|
+
subfolder = winreg.EnumKey(root_key, i)
|
721
|
+
with winreg.OpenKey(
|
722
|
+
HKEY_CURRENT_USER, f"{root}\\{subfolder}", 0, KEY_READ
|
723
|
+
) as key:
|
724
|
+
try:
|
725
|
+
mount_point, _ = winreg.QueryValueEx(key, "MountPoint")
|
726
|
+
url_namespace, _ = winreg.QueryValueEx(key, "URLNamespace")
|
727
|
+
url_to_mount[url_namespace] = mount_point
|
728
|
+
except FileNotFoundError:
|
729
|
+
pass
|
730
|
+
except FileNotFoundError:
|
731
|
+
pass
|
732
|
+
return url_to_mount
|
733
|
+
else:
|
734
|
+
return {}
|
735
|
+
|
736
|
+
|
737
|
+
def search_local_sharepoint_path(url, root, sharepoint_config, sharepoint_config_name):
|
738
|
+
book_name = url.split("/")[-1]
|
739
|
+
local_book_paths = []
|
740
|
+
for path in Path(root).rglob("[!~$]*.xls*"):
|
741
|
+
if path.name.lower() == book_name.lower():
|
742
|
+
local_book_paths.append(path)
|
743
|
+
if len(local_book_paths) == 1:
|
744
|
+
return str(local_book_paths[0])
|
745
|
+
elif len(local_book_paths) == 0:
|
746
|
+
raise xlwings.XlwingsError(
|
747
|
+
"Couldn't find your SharePoint file locally, see: xlwings.org/error"
|
748
|
+
)
|
749
|
+
else:
|
750
|
+
raise xlwings.XlwingsError(
|
751
|
+
f"Your SharePoint configuration either requires your workbook name to be "
|
752
|
+
f"unique across all synced SharePoint folders or you need to "
|
753
|
+
f"{'edit' if sharepoint_config else 'add'} the {sharepoint_config_name} "
|
754
|
+
f"setting including one or more folder levels, see: xlwings.org/error."
|
755
|
+
)
|
756
|
+
|
757
|
+
|
758
|
+
def excel_update_picture(picture_impl, filename):
|
759
|
+
name = picture_impl.name
|
760
|
+
left, top = picture_impl.left, picture_impl.top
|
761
|
+
width, height = picture_impl.width, picture_impl.height
|
762
|
+
|
763
|
+
picture_impl.delete()
|
764
|
+
|
765
|
+
picture_impl = picture_impl.parent.pictures.add(
|
766
|
+
filename,
|
767
|
+
link_to_file=False,
|
768
|
+
save_with_document=True,
|
769
|
+
left=left,
|
770
|
+
top=top,
|
771
|
+
width=width,
|
772
|
+
height=height,
|
773
|
+
anchor=None,
|
774
|
+
)
|
775
|
+
|
776
|
+
picture_impl.name = name
|
777
|
+
return picture_impl
|