xlwings-utils 25.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,794 @@
1
+ # _ _ _ _ _
2
+ # __ __| |__ __(_) _ __ __ _ ___ _ _ | |_ (_)| | ___
3
+ # \ \/ /| |\ \ /\ / /| || '_ \ / _` |/ __| | | | || __|| || |/ __|
4
+ # > < | | \ V V / | || | | || (_| |\__ \ | |_| || |_ | || |\__ \
5
+ # /_/\_\|_| \_/\_/ |_||_| |_| \__, ||___/ _____ \__,_| \__||_||_||___/
6
+ # |___/ |_____|
7
+
8
+ __version__ = "25.2.1"
9
+
10
+ from pathlib import Path
11
+ import sys
12
+ import math
13
+ import base64
14
+ import datetime
15
+ import functools
16
+
17
+ Pythonista = sys.platform == "ios"
18
+
19
+ try:
20
+ import xlwings
21
+ except ImportError:
22
+ xlwings = False
23
+
24
+ missing = object()
25
+
26
+
27
+ class block:
28
+ """
29
+ block is 2 dimensional data structure with 1 as lowest index (like xlwings range)
30
+
31
+ Parameters
32
+ ----------
33
+ number_of_rows : int
34
+ number of rows (dedault 1)
35
+
36
+ number_of_columns : int
37
+ number of columns (default 1)
38
+
39
+ Returns
40
+ -------
41
+ block
42
+ """
43
+
44
+ def __init__(self, number_of_rows=1, number_of_columns=1):
45
+ self.dict = {}
46
+ self.number_of_rows = number_of_rows
47
+ self.number_of_columns = number_of_columns
48
+ self._invalidate_highest_used_cache()
49
+
50
+ def __eq__(self, other):
51
+ if isinstance(other, block):
52
+ return self.value == other.value
53
+ return False
54
+
55
+ @classmethod
56
+ def from_value(cls, value, column_like=False):
57
+ """
58
+ makes a block from a given value
59
+
60
+ Parameters
61
+ ----------
62
+ value : scalar, list of scalars, list of lists of scalars or block
63
+ value to be used in block, possibly expanded to a list of lists of scalars
64
+
65
+ column_like : boolean
66
+ if value is a list of scalars, values is interpreted as a column if True, as a row otherwise
67
+
68
+ Returns
69
+ -------
70
+ block : block
71
+ """
72
+ if isinstance(value, block):
73
+ value = value.value
74
+ if not isinstance(value, list):
75
+ value = [[value]]
76
+ if not isinstance(value[0], list):
77
+ if column_like:
78
+ value = [[item] for item in value]
79
+ else:
80
+ value = [value]
81
+ bl = cls(len(value), 1)
82
+
83
+ for row, row_contents in enumerate(value, 1):
84
+ for column, item in enumerate(row_contents, 1):
85
+ if item and not (isinstance(item, float) and math.isnan(item)):
86
+ bl.dict[row, column] = item
87
+ bl._number_of_columns = max(bl.number_of_columns, column)
88
+ return bl
89
+
90
+ @classmethod
91
+ def from_range(cls, rng):
92
+ """
93
+ makes a block from a given range
94
+
95
+ Parameters
96
+ ----------
97
+ rng : xlwings.Range
98
+ range to be used be used in block
99
+
100
+ Returns
101
+ -------
102
+ block : block
103
+ """
104
+ number_of_rows, number_of_columns = rng.shape
105
+ return cls.from_value(rng.value, column_like=(number_of_columns == 1))
106
+
107
+ @classmethod
108
+ def from_xlrd_sheet(cls, sheet):
109
+ """
110
+ makes a block from a xlrd sheet
111
+
112
+ Parameters
113
+ ----------
114
+ sheet : xlrd sheet
115
+ sheet to be used be used in block
116
+
117
+ Returns
118
+ -------
119
+ block : block
120
+ """
121
+ v = [sheet.row_values(row_idx)[0 : sheet.ncols] for row_idx in range(0, sheet.nrows)]
122
+ return cls.from_value(v)
123
+
124
+ @classmethod
125
+ def from_openpyxl_sheet(cls, sheet):
126
+ """
127
+ makes a block from an openpyxl sheet
128
+
129
+ Parameters
130
+ ----------
131
+ sheet : xlrd sheet
132
+ sheet to be used be used in block
133
+
134
+ Returns
135
+ -------
136
+ block : block
137
+ """
138
+ v = [[cell.value for cell in row] for row in sheet.iter_rows()]
139
+ return cls.from_value(v)
140
+
141
+ @classmethod
142
+ def from_file(cls, filename):
143
+ """
144
+ makes a block from a file
145
+
146
+ Parameters
147
+ ----------
148
+ filename : str
149
+ file to be used be used in block
150
+
151
+ Returns
152
+ -------
153
+ block : block
154
+ """
155
+ with open(filename, "r") as f:
156
+ v = [[line if line else missing] for line in f.read().splitlines()]
157
+ return cls.from_value(v)
158
+
159
+ @classmethod
160
+ def from_dataframe(cls, df):
161
+ """
162
+ makes a block from a given dataframe
163
+
164
+ Parameters
165
+ ----------
166
+ df : pandas dataframe
167
+ dataframe to be used be used in block
168
+
169
+ Returns
170
+ -------
171
+ block : block
172
+ """
173
+ v = df.values.tolist()
174
+ return cls.from_value(v)
175
+
176
+ def to_openpyxl_sheet(self, sheet):
177
+ """
178
+ appends a block to a given openpyxl sheet
179
+
180
+ Parameters
181
+ ----------
182
+ sheet: openpyxl sheet
183
+ sheet to be used be used
184
+
185
+ Returns
186
+ -------
187
+ block : block
188
+ """
189
+ for row in self.value:
190
+ sheet.append(row)
191
+
192
+ def reshape(self, number_of_rows=missing, number_of_columns=missing):
193
+ """
194
+ makes a new block with given dimensions
195
+
196
+ Parameters
197
+ ----------
198
+ number_of_rows : int
199
+ if given, expand or shrink to the given number of rows
200
+
201
+ number_of_columns : int
202
+ if given, expand or shrink to the given number of columns
203
+
204
+ Returns
205
+ -------
206
+ block : block
207
+ """
208
+ if number_of_rows is missing:
209
+ number_of_rows = self.number_of_rows
210
+ if number_of_columns is missing:
211
+ number_of_columns = self.number_of_columns
212
+ bl = block(number_of_rows=number_of_rows, number_of_columns=number_of_columns)
213
+ for (row, column), value in self.dict.items():
214
+ if row <= number_of_rows and column <= number_of_columns:
215
+ bl[row, column] = value
216
+ return bl
217
+
218
+ @property
219
+ def value(self):
220
+ return [[self.dict.get((row, column)) for column in range(1, self.number_of_columns + 1)] for row in range(1, self.number_of_rows + 1)]
221
+
222
+ def _invalidate_highest_used_cache(self):
223
+ self._highest_used_row_number = None
224
+ self._highest_used_column_number = None
225
+
226
+ def __setitem__(self, row_column, value):
227
+ row, column = row_column
228
+ if row < 1 or row > self.number_of_rows:
229
+ raise IndexError(f"row must be between 1 and {self.number_of_rows}; not {row}")
230
+ if column < 1 or column > self.number_of_columns:
231
+ raise IndexError(f"column must be between 1 and {self.number_of_columns}; not {column}")
232
+ if value is None:
233
+ if (row, column) in self.dict:
234
+ del self.dict[row, column]
235
+ self._invalidate_highest_used_cache()
236
+
237
+ else:
238
+ self.dict[row, column] = value
239
+ if self._highest_used_row_number:
240
+ self._highest_used_row_number = max(self._highest_used_row_number, row)
241
+ if self._highest_used_column_number:
242
+ self._highest_used_column_number = max(self._highest_used_column_number, column)
243
+
244
+ def __getitem__(self, row_column):
245
+ row, column = row_column
246
+ if row < 1 or row > self.number_of_rows:
247
+ raise IndexError(f"row must be between 1 and {self.number_of_rows} not {row}")
248
+ if column < 1 or column > self.number_of_columns:
249
+ raise IndexError(f"column must be between 1 and {self.number_of_columns} not {column}")
250
+ return self.dict.get((row, column))
251
+
252
+ def minimized(self):
253
+ """
254
+ Returns
255
+ -------
256
+ minimized block : block
257
+ uses highest_used_row_number and highest_used_column_number to minimize the block
258
+ """
259
+ return self.reshape(number_of_rows=self.highest_used_row_number, number_of_columns=self.highest_used_column_number)
260
+
261
+ @property
262
+ def number_of_rows(self):
263
+ return self._number_of_rows
264
+
265
+ @number_of_rows.setter
266
+ def number_of_rows(self, value):
267
+ if value < 1:
268
+ raise ValueError(f"number_of_rows should be >=1; not {value}")
269
+ self._invalidate_highest_used_cache()
270
+ self._number_of_rows = value
271
+ for row, column in list(self.dict):
272
+ if row > self._number_of_rows:
273
+ del self.dict[row, column]
274
+
275
+ @property
276
+ def number_of_columns(self):
277
+ return self._number_of_columns
278
+
279
+ @number_of_columns.setter
280
+ def number_of_columns(self, value):
281
+ if value < 1:
282
+ raise ValueError(f"number_of_columns should be >=1; not {value}")
283
+ self._invalidate_highest_used_cache()
284
+ self._number_of_columns = value
285
+ for row, column in list(self.dict):
286
+ if column > self._number_of_columns:
287
+ del self.dict[row, column]
288
+
289
+ @property
290
+ def highest_used_row_number(self):
291
+ if not self._highest_used_row_number:
292
+ if self.dict:
293
+ self._highest_used_row_number = max(row for (row, column) in self.dict)
294
+ else:
295
+ self._highest_used_row_number = 1
296
+ return self._highest_used_row_number
297
+
298
+ @property
299
+ def highest_used_column_number(self):
300
+ if not self._highest_used_column_number:
301
+ if self.dict:
302
+ self._highest_used_column_number = max(column for (row, column) in self.dict)
303
+ else:
304
+ self._highest_used_column_number = 1
305
+
306
+ return self._highest_used_column_number
307
+
308
+ def __repr__(self):
309
+ return f"block({self.value})"
310
+
311
+ def _check_row(self, row, name):
312
+ if row < 1:
313
+ raise ValueError(f"{name}={row} < 1")
314
+ if row > self.number_of_rows:
315
+ raise ValueError(f"{name}={row} > number_of_rows={self.number_of_rows}")
316
+
317
+ def _check_column(self, column, name):
318
+ if column < 1:
319
+ raise ValueError(f"{name}={column} < 1")
320
+ if column > self.number_of_columns:
321
+ raise ValueError(f"{name}={column} > number_of_columns={self.number_of_columns}")
322
+
323
+ def transposed(self):
324
+ """
325
+ transpose block
326
+
327
+ Returns
328
+ -------
329
+ transposed block : block
330
+ """
331
+ bl = block(number_of_rows=self.number_of_columns, number_of_columns=self.number_of_rows)
332
+ for (row, column), value in self.dict.items():
333
+ bl[column, row] = value
334
+ return bl
335
+
336
+ def vlookup(self, s, *, row_from=1, row_to=missing, column1=1, column2=missing, default=missing):
337
+ """
338
+ searches in column1 for row between row_from and row_to for s and returns the value found at (that row, column2)
339
+
340
+ Parameters
341
+ ----------
342
+ s : any
343
+ value to seach for
344
+
345
+ row_from : int
346
+ row to start search (default 1)
347
+
348
+ should be between 1 and number_of_rows
349
+
350
+ row_to : int
351
+ row to end search (default number_of_rows)
352
+
353
+ should be between 1 and number_of_rows
354
+
355
+ column1 : int
356
+ column to search in (default 1)
357
+
358
+ should be between 1 and number_of_columns
359
+
360
+ column2 : int
361
+ column to return looked up value from (default column1 + 1)
362
+
363
+ should be between 1 and number_of_columns
364
+
365
+ default : any
366
+ if s is not found, returns the default.
367
+
368
+ if omitted, a ValueError exception will be raised in that case
369
+
370
+ Returns
371
+ -------
372
+ block[found row number, column2] : any
373
+ """
374
+ if column2 is missing:
375
+ column2 = column1 + 1
376
+ self._check_column(column2, "column2")
377
+ row = self.lookup_row(s, row_from=row_from, row_to=row_to, column1=column1, default=-1)
378
+ if row == -1:
379
+ if default is missing:
380
+ raise ValueError(f"{s} not found]")
381
+ else:
382
+ return default
383
+ else:
384
+ return self[row, column2]
385
+
386
+ def lookup_row(self, s, *, row_from=1, row_to=missing, column1=1, default=missing):
387
+ """
388
+ searches in column1 for row between row_from and row_to for s and returns that row number
389
+
390
+ Parameters
391
+ ----------
392
+ s : any
393
+ value to seach for
394
+
395
+ row_from : int
396
+ row to start search (default 1)
397
+
398
+ should be between 1 and number_of_rows
399
+
400
+ row_to : int
401
+ row to end search (default number_of_rows)
402
+
403
+ should be between 1 and number_of_rows
404
+
405
+ column1 : int
406
+ column to search in (default 1)
407
+
408
+ should be between 1 and number_of_columns
409
+
410
+ column2 : int
411
+ column to return looked up value from (default column1 + 1)
412
+
413
+ default : any
414
+ if s is not found, returns the default.
415
+
416
+ if omitted, a ValueError exception will be raised
417
+
418
+ default : any
419
+ if s is not found, returns the default.
420
+
421
+ if omitted, a ValueError exception will be raised in that case
422
+
423
+
424
+ Returns
425
+ -------
426
+ row number where block[row nunber, column1] == s : int
427
+ """
428
+ if row_to is missing:
429
+ row_to = self.highest_used_row_number
430
+ self._check_row(row_from, "row_from")
431
+ self._check_row(row_to, "row_to")
432
+ self._check_column(column1, "column1")
433
+
434
+ for row in range(row_from, row_to + 1):
435
+ if self[row, column1] == s:
436
+ return row
437
+ if default is missing:
438
+ raise ValueError(f"{s} not found")
439
+ else:
440
+ return default
441
+
442
+ def hlookup(self, s, *, column_from=1, column_to=missing, row1=1, row2=missing, default=missing):
443
+ """
444
+ searches in row1 for column between column_from and column_to for s and returns the value found at (that column, row2)
445
+
446
+ Parameters
447
+ ----------
448
+ s : any
449
+ value to seach for
450
+
451
+ column_from : int
452
+ column to start search (default 1)
453
+
454
+ should be between 1 and number_of_columns
455
+
456
+ column_to : int
457
+ column to end search (default number_of_columns)
458
+
459
+ should be between 1 and number_of_columns
460
+
461
+ row1 : int
462
+ row to search in (default 1)
463
+
464
+ should be between 1 and number_of_rows
465
+
466
+ row2 : int
467
+ row to return looked up value from (default row1 + 1)
468
+
469
+ should be between 1 and number_of_rows
470
+
471
+ default : any
472
+ if s is not found, returns the default.
473
+
474
+ if omitted, a ValueError exception will be raised in that case
475
+
476
+ Returns
477
+ -------
478
+ block[row, found column, row2] : any
479
+ """
480
+ if row2 is missing:
481
+ row2 = row1 + 1
482
+ self._check_row(row2, "row2")
483
+ column = self.lookup_column(s, column_from=column_from, column_to=column_to, row1=row1, default=-1)
484
+ if column == -1:
485
+ if default is missing:
486
+ raise ValueError(f"{s} not found")
487
+ else:
488
+ return default
489
+ else:
490
+ return self[row2, column]
491
+
492
+ def lookup_column(self, s, *, column_from=1, column_to=missing, row1=1, default=missing):
493
+ """
494
+ searches in row1 for column between column_from and column_to for s and returns that column number
495
+
496
+ Parameters
497
+ ----------
498
+ s : any
499
+ value to seach for
500
+
501
+ column_from : int
502
+ column to start search (default 1)
503
+
504
+ should be between 1 and number_of_columns
505
+
506
+ column_to : int
507
+ column to end search (default number_of_columns)
508
+
509
+ should be between 1 and number_of_columns
510
+
511
+ row1 : int
512
+ row to search in (default 1)
513
+
514
+ should be between 1 and number_of_rows
515
+
516
+ row2 : int
517
+ row to return looked up value from (default row1 + 1)
518
+
519
+ default : any
520
+ if s is not found, returns the default.
521
+
522
+ if omitted, a ValueError exception will be raised in that case
523
+
524
+ Returns
525
+ -------
526
+ column number where block[row1, column number] == s : int
527
+ """
528
+ if column_to is missing:
529
+ column_to = self.highest_used_column_number
530
+ self._check_column(column_from, "column_from")
531
+ self._check_column(column_to, "column_to")
532
+ self._check_row(row1, "row1")
533
+
534
+ for column in range(column_from, column_to + 1):
535
+ if self[row1, column] == s:
536
+ return column
537
+ if default is missing:
538
+ raise ValueError(f"{s} not found")
539
+ else:
540
+ return default
541
+
542
+ def lookup(self, s, *, row_from=1, row_to=missing, column1=1, column2=missing, default=missing):
543
+ """
544
+ searches in column1 for row between row_from and row_to for s and returns the value found at (that row, column2)
545
+
546
+ Parameters
547
+ ----------
548
+ s : any
549
+ value to seach for
550
+
551
+ row_from : int
552
+ row to start search (default 1)
553
+
554
+ should be between 1 and number_of_rows
555
+
556
+ row_to : int
557
+ row to end search (default number_of_rows)
558
+
559
+ should be between 1 and number_of_rows
560
+
561
+ column1 : int
562
+ column to search in (default 1)
563
+
564
+ should be between 1 and number_of_columns
565
+
566
+ column2 : int
567
+ column to return looked up value from (default column1 + 1)
568
+
569
+ should be between 1 and number_of_columns
570
+
571
+ default : any
572
+ if s is not found, returns the default.
573
+
574
+ if omitted, a ValueError exception will be raised in that case
575
+
576
+ Returns
577
+ -------
578
+ block[found row number, column2] : any
579
+
580
+ Note
581
+ ----
582
+ This is exactly the same as vlookup.
583
+ """
584
+ return self.vlookup(s, row_from=row_from, row_to=row_to, column1=column1, column2=column2, default=default)
585
+
586
+ def decode_to_files(self):
587
+ """
588
+ decode the block with encoded file(s) to individual pyoidide file(s)
589
+
590
+ Returns
591
+ -------
592
+ count : int
593
+ number of files decoded
594
+
595
+ Note
596
+ ----
597
+ if the block does not contain an encode file, the method just returns 0
598
+ """
599
+ count = 0
600
+ for column in range(1, self.number_of_columns + 1):
601
+ row = 1
602
+ bl = self.minimized()
603
+ while row <= self.number_of_rows:
604
+ if self[row, column] and self[row, column].startswith("<file=") and self[row, column].endswith(">"):
605
+ filename = self[row, column][6:-1]
606
+ collect = []
607
+ row += 1
608
+ while bl[row, column] != "</file>":
609
+ if bl[row, column]:
610
+ collect.append(bl[row, column])
611
+ row += 1
612
+ decoded = base64.b64decode("".join(collect))
613
+ open(filename, "wb").write(decoded)
614
+ count += 1
615
+ row += 1
616
+ return count
617
+
618
+ @classmethod
619
+ def encode_file(cls, file):
620
+ """
621
+ make a block with the given pyodide file encoded
622
+
623
+ Parameters
624
+ ----------
625
+ file : file name (str)
626
+ file to be encoded
627
+
628
+ Returns
629
+ -------
630
+ block with encoded file : block (minimized)
631
+ """
632
+
633
+ bl = cls(number_of_rows=100000, number_of_columns=1)
634
+
635
+ n = 5000 # block size
636
+ row = 1
637
+ bl[row, 1] = f"<file={file}>"
638
+ row += 1
639
+ b64 = base64.b64encode(open(file, "rb").read()).decode("utf-8")
640
+ while b64:
641
+ b64_n = b64[:n]
642
+ bl[row, 1] = b64_n
643
+ row += 1
644
+ b64 = b64[n:]
645
+ bl[row, 1] = f"</file>"
646
+ row += 1
647
+ return bl.minimized()
648
+
649
+
650
+ class Capture:
651
+ """
652
+ specifies how to capture stdout
653
+
654
+ Parameters
655
+ ----------
656
+ enabled : bool
657
+ if True (default), all stdout output is captured
658
+
659
+ if False, stdout output is printed
660
+
661
+ include_print : bool
662
+ if False (default), nothing will be printed if enabled is True
663
+
664
+ if True, output will be printed (and captured if enabled is True)
665
+
666
+ Note
667
+ ----
668
+ Use this like ::
669
+
670
+ capture = xwu.Capture()
671
+ """
672
+
673
+ _instance = None
674
+
675
+ def __new__(cls, *args, **kwargs):
676
+ # singleton
677
+ if cls._instance is None:
678
+ cls._instance = super(Capture, cls).__new__(cls)
679
+ return cls._instance
680
+
681
+ def __init__(self, enabled=missing, include_print=missing):
682
+ if hasattr(self, "stdout"):
683
+ if enabled is not missing:
684
+ self.enabled = enabled
685
+ if include_print is not missing:
686
+ self.include_print = include_print
687
+ return
688
+ self.stdout = sys.stdout
689
+ self._buffer = []
690
+ self.enabled = True if enabled is missing else enabled
691
+ self.include_print = False if include_print is missing else include_print
692
+
693
+ def __call__(self, enabled=missing, include_print=missing):
694
+ return self.__class__(enabled, include_print)
695
+
696
+ def __enter__(self):
697
+ self.enabled = True
698
+
699
+ def __exit__(self, exc_type, exc_value, tb):
700
+ self.enabled = False
701
+
702
+ def write(self, data):
703
+ self._buffer.append(data)
704
+ if self._include_print:
705
+ self.stdout.write(data)
706
+
707
+ def flush(self):
708
+ if self._include_print:
709
+ self.stdout.flush()
710
+ self._buffer.append("\n")
711
+
712
+ @property
713
+ def enabled(self):
714
+ return sys.out == self
715
+
716
+ @enabled.setter
717
+ def enabled(self, value):
718
+ if value:
719
+ sys.stdout = self
720
+ else:
721
+ sys.stdout = self.stdout
722
+
723
+ @property
724
+ def value(self):
725
+ result = self.value_keep
726
+ self.clear()
727
+ return result
728
+
729
+ @property
730
+ def value_keep(self):
731
+ result = [[line] for line in self.str_keep.splitlines()]
732
+ return result
733
+
734
+ @property
735
+ def str(self):
736
+ result = self.str_keep
737
+ self._buffer.clear()
738
+ return result
739
+
740
+ @property
741
+ def str_keep(self):
742
+ result = "".join(self._buffer)
743
+ return result
744
+
745
+ def clear(self):
746
+ self._buffer.clear()
747
+
748
+ @property
749
+ def include_print(self):
750
+ return self._include_print
751
+
752
+ @include_print.setter
753
+ def include_print(self, value):
754
+ self._include_print = value
755
+
756
+
757
+ def trigger_macro(sheet):
758
+ """
759
+ triggers the macro on sheet
760
+
761
+ Parameters
762
+ ----------
763
+ sheet : sheet
764
+ sheet to use
765
+
766
+ """
767
+
768
+ sheet["A1"].value = "=NOW()"
769
+
770
+
771
+ def timer(func):
772
+ """
773
+ this decorator should be placed after the @xw.script decorator
774
+
775
+ it will show the name, entry time, exit time and the duration, like
776
+ Done MyScript 11:51:13.24 - 11:51:20.28 (7.04s)
777
+
778
+ """
779
+
780
+ @functools.wraps(func)
781
+ def wrapper(*args, **kwargs):
782
+ now0 = datetime.datetime.now()
783
+ result = func(*args, **kwargs)
784
+ now1 = datetime.datetime.now()
785
+ diff = (now1 - now0).total_seconds()
786
+ print(f"{now0:%H:%M:%S.}{int(now0.microsecond / 10000):02d} - {now1:%H:%M:%S.}{int(now1.microsecond / 10000):02d} ({diff:.2f}s)")
787
+
788
+ return result
789
+
790
+ return wrapper
791
+
792
+
793
+ if __name__ == "__main__":
794
+ ...