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.
Files changed (101) hide show
  1. tdrpa/_tdxlwings/__init__.py +193 -0
  2. tdrpa/_tdxlwings/__pycache__/__init__.cpython-311.pyc +0 -0
  3. tdrpa/_tdxlwings/__pycache__/__init__.cpython-38.pyc +0 -0
  4. tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-311.pyc +0 -0
  5. tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-38.pyc +0 -0
  6. tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-311.pyc +0 -0
  7. tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-38.pyc +0 -0
  8. tdrpa/_tdxlwings/__pycache__/apps.cpython-311.pyc +0 -0
  9. tdrpa/_tdxlwings/__pycache__/apps.cpython-38.pyc +0 -0
  10. tdrpa/_tdxlwings/__pycache__/base_classes.cpython-311.pyc +0 -0
  11. tdrpa/_tdxlwings/__pycache__/base_classes.cpython-38.pyc +0 -0
  12. tdrpa/_tdxlwings/__pycache__/com_server.cpython-311.pyc +0 -0
  13. tdrpa/_tdxlwings/__pycache__/com_server.cpython-38.pyc +0 -0
  14. tdrpa/_tdxlwings/__pycache__/constants.cpython-311.pyc +0 -0
  15. tdrpa/_tdxlwings/__pycache__/constants.cpython-38.pyc +0 -0
  16. tdrpa/_tdxlwings/__pycache__/expansion.cpython-311.pyc +0 -0
  17. tdrpa/_tdxlwings/__pycache__/expansion.cpython-38.pyc +0 -0
  18. tdrpa/_tdxlwings/__pycache__/main.cpython-311.pyc +0 -0
  19. tdrpa/_tdxlwings/__pycache__/main.cpython-38.pyc +0 -0
  20. tdrpa/_tdxlwings/__pycache__/udfs.cpython-311.pyc +0 -0
  21. tdrpa/_tdxlwings/__pycache__/udfs.cpython-38.pyc +0 -0
  22. tdrpa/_tdxlwings/__pycache__/utils.cpython-311.pyc +0 -0
  23. tdrpa/_tdxlwings/__pycache__/utils.cpython-38.pyc +0 -0
  24. tdrpa/_tdxlwings/_win32patch.py +90 -0
  25. tdrpa/_tdxlwings/_xlmac.py +2240 -0
  26. tdrpa/_tdxlwings/_xlwindows.py +2518 -0
  27. tdrpa/_tdxlwings/addin/Dictionary.cls +474 -0
  28. tdrpa/_tdxlwings/addin/IWebAuthenticator.cls +71 -0
  29. tdrpa/_tdxlwings/addin/WebClient.cls +772 -0
  30. tdrpa/_tdxlwings/addin/WebHelpers.bas +3203 -0
  31. tdrpa/_tdxlwings/addin/WebRequest.cls +875 -0
  32. tdrpa/_tdxlwings/addin/WebResponse.cls +453 -0
  33. tdrpa/_tdxlwings/addin/xlwings.xlam +0 -0
  34. tdrpa/_tdxlwings/apps.py +35 -0
  35. tdrpa/_tdxlwings/base_classes.py +1092 -0
  36. tdrpa/_tdxlwings/cli.py +1306 -0
  37. tdrpa/_tdxlwings/com_server.py +385 -0
  38. tdrpa/_tdxlwings/constants.py +3080 -0
  39. tdrpa/_tdxlwings/conversion/__init__.py +103 -0
  40. tdrpa/_tdxlwings/conversion/framework.py +147 -0
  41. tdrpa/_tdxlwings/conversion/numpy_conv.py +34 -0
  42. tdrpa/_tdxlwings/conversion/pandas_conv.py +184 -0
  43. tdrpa/_tdxlwings/conversion/standard.py +321 -0
  44. tdrpa/_tdxlwings/expansion.py +83 -0
  45. tdrpa/_tdxlwings/ext/__init__.py +3 -0
  46. tdrpa/_tdxlwings/ext/sql.py +73 -0
  47. tdrpa/_tdxlwings/html/xlwings-alert.html +71 -0
  48. tdrpa/_tdxlwings/js/xlwings.js +577 -0
  49. tdrpa/_tdxlwings/js/xlwings.ts +729 -0
  50. tdrpa/_tdxlwings/mac_dict.py +6399 -0
  51. tdrpa/_tdxlwings/main.py +5205 -0
  52. tdrpa/_tdxlwings/mistune/__init__.py +63 -0
  53. tdrpa/_tdxlwings/mistune/block_parser.py +366 -0
  54. tdrpa/_tdxlwings/mistune/inline_parser.py +216 -0
  55. tdrpa/_tdxlwings/mistune/markdown.py +84 -0
  56. tdrpa/_tdxlwings/mistune/renderers.py +220 -0
  57. tdrpa/_tdxlwings/mistune/scanner.py +121 -0
  58. tdrpa/_tdxlwings/mistune/util.py +41 -0
  59. tdrpa/_tdxlwings/pro/__init__.py +40 -0
  60. tdrpa/_tdxlwings/pro/_xlcalamine.py +536 -0
  61. tdrpa/_tdxlwings/pro/_xlofficejs.py +146 -0
  62. tdrpa/_tdxlwings/pro/_xlremote.py +1293 -0
  63. tdrpa/_tdxlwings/pro/custom_functions_code.js +150 -0
  64. tdrpa/_tdxlwings/pro/embedded_code.py +60 -0
  65. tdrpa/_tdxlwings/pro/udfs_officejs.py +549 -0
  66. tdrpa/_tdxlwings/pro/utils.py +199 -0
  67. tdrpa/_tdxlwings/quickstart.xlsm +0 -0
  68. tdrpa/_tdxlwings/quickstart_addin.xlam +0 -0
  69. tdrpa/_tdxlwings/quickstart_addin_ribbon.xlam +0 -0
  70. tdrpa/_tdxlwings/quickstart_fastapi/main.py +47 -0
  71. tdrpa/_tdxlwings/quickstart_fastapi/requirements.txt +3 -0
  72. tdrpa/_tdxlwings/quickstart_standalone.xlsm +0 -0
  73. tdrpa/_tdxlwings/reports.py +12 -0
  74. tdrpa/_tdxlwings/rest/__init__.py +1 -0
  75. tdrpa/_tdxlwings/rest/api.py +368 -0
  76. tdrpa/_tdxlwings/rest/serializers.py +103 -0
  77. tdrpa/_tdxlwings/server.py +14 -0
  78. tdrpa/_tdxlwings/udfs.py +775 -0
  79. tdrpa/_tdxlwings/utils.py +777 -0
  80. tdrpa/_tdxlwings/xlwings-0.31.6.applescript +30 -0
  81. tdrpa/_tdxlwings/xlwings.bas +2061 -0
  82. tdrpa/_tdxlwings/xlwings_custom_addin.bas +2042 -0
  83. tdrpa/_tdxlwings/xlwingslib.cp38-win_amd64.pyd +0 -0
  84. tdrpa/tdworker/__init__.pyi +12 -0
  85. tdrpa/tdworker/_clip.pyi +50 -0
  86. tdrpa/tdworker/_excel.pyi +743 -0
  87. tdrpa/tdworker/_file.pyi +77 -0
  88. tdrpa/tdworker/_img.pyi +226 -0
  89. tdrpa/tdworker/_network.pyi +94 -0
  90. tdrpa/tdworker/_os.pyi +47 -0
  91. tdrpa/tdworker/_sp.pyi +21 -0
  92. tdrpa/tdworker/_w.pyi +129 -0
  93. tdrpa/tdworker/_web.pyi +995 -0
  94. tdrpa/tdworker/_winE.pyi +228 -0
  95. tdrpa/tdworker/_winK.pyi +74 -0
  96. tdrpa/tdworker/_winM.pyi +117 -0
  97. tdrpa/tdworker.cp312-win_amd64.pyd +0 -0
  98. tdrpa_tdworker-1.2.13.2.dist-info/METADATA +38 -0
  99. tdrpa_tdworker-1.2.13.2.dist-info/RECORD +101 -0
  100. tdrpa_tdworker-1.2.13.2.dist-info/WHEEL +5 -0
  101. 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