xlwings-utils 25.0.0.post1__py3-none-any.whl → 25.0.0.post3__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.
Potentially problematic release.
This version of xlwings-utils might be problematic. Click here for more details.
- xlwings_utils/xlwings_utils.py +227 -4
- {xlwings_utils-25.0.0.post1.dist-info → xlwings_utils-25.0.0.post3.dist-info}/METADATA +71 -3
- xlwings_utils-25.0.0.post3.dist-info/RECORD +6 -0
- {xlwings_utils-25.0.0.post1.dist-info → xlwings_utils-25.0.0.post3.dist-info}/WHEEL +1 -1
- xlwings_utils-25.0.0.post1.dist-info/RECORD +0 -6
- {xlwings_utils-25.0.0.post1.dist-info → xlwings_utils-25.0.0.post3.dist-info}/top_level.txt +0 -0
xlwings_utils/xlwings_utils.py
CHANGED
|
@@ -12,6 +12,7 @@ import dropbox
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
import math
|
|
15
16
|
|
|
16
17
|
dbx = None
|
|
17
18
|
Pythonista = sys.platform == "ios"
|
|
@@ -287,6 +288,31 @@ class block:
|
|
|
287
288
|
if number_of_columns is not None:
|
|
288
289
|
self.number_of_columns = number_of_columns
|
|
289
290
|
|
|
291
|
+
@classmethod
|
|
292
|
+
def from_xlrd_sheet(cls, sheet, number_of_rows=None, number_of_columns=None):
|
|
293
|
+
v = [sheet.row_values(row_idx)[0 : sheet.ncols] for row_idx in range(0, sheet.nrows)]
|
|
294
|
+
return cls(v, number_of_rows=number_of_rows, number_of_columns=number_of_columns)
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def from_openpyxl_sheet(cls, sheet, number_of_rows=None, number_of_columns=None):
|
|
298
|
+
v = [[cell.value for cell in row] for row in sheet.iter_rows()]
|
|
299
|
+
return cls(v, number_of_rows=number_of_rows, number_of_columns=number_of_columns)
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def from_file(cls, filename, number_of_rows=None, number_of_columns=None):
|
|
303
|
+
with open(filename, "r") as f:
|
|
304
|
+
v = [[line if line else None] for line in f.read().splitlines()]
|
|
305
|
+
return cls(v, number_of_rows=number_of_rows, number_of_columns=number_of_columns)
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def from_dataframe(cls, df, number_of_rows=None, number_of_columns=None):
|
|
309
|
+
v = df.values.tolist()
|
|
310
|
+
return cls(v, number_of_rows=number_of_rows, number_of_columns=number_of_columns)
|
|
311
|
+
|
|
312
|
+
def to_openpyxl_sheet(self, sheet):
|
|
313
|
+
for row in self.value:
|
|
314
|
+
sheet.append(row)
|
|
315
|
+
|
|
290
316
|
@property
|
|
291
317
|
def value(self):
|
|
292
318
|
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)]
|
|
@@ -306,7 +332,7 @@ class block:
|
|
|
306
332
|
|
|
307
333
|
for row, row_contents in enumerate(value, 1):
|
|
308
334
|
for column, item in enumerate(row_contents, 1):
|
|
309
|
-
if item
|
|
335
|
+
if item and not (isinstance(item, float) and math.isnan(item)):
|
|
310
336
|
self.dict[row, column] = item
|
|
311
337
|
self._number_of_columns = max(self.number_of_columns, column)
|
|
312
338
|
|
|
@@ -327,6 +353,12 @@ class block:
|
|
|
327
353
|
return self.dict.get((row, column))
|
|
328
354
|
|
|
329
355
|
def minimized(self):
|
|
356
|
+
"""
|
|
357
|
+
Returns
|
|
358
|
+
-------
|
|
359
|
+
minimized block : block
|
|
360
|
+
uses highest_used_row_number and highest_used_column_number to minimize the block
|
|
361
|
+
"""
|
|
330
362
|
return block(self, number_of_rows=self.highest_used_row_number, number_of_columns=self.highest_used_column_number)
|
|
331
363
|
|
|
332
364
|
@property
|
|
@@ -385,6 +417,42 @@ class block:
|
|
|
385
417
|
raise ValueError(f"{name}={column} > number_of_columns={self.number_of_columns}")
|
|
386
418
|
|
|
387
419
|
def vlookup(self, s, *, row_from=1, row_to=None, column1=1, column2=None):
|
|
420
|
+
"""
|
|
421
|
+
searches in column1 for row between row_from and row_to for s and returns the value found at (that row, column2)
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
s : any
|
|
426
|
+
value to seach for
|
|
427
|
+
|
|
428
|
+
row_from : int
|
|
429
|
+
row to start search (default 1)
|
|
430
|
+
|
|
431
|
+
should be between 1 and number_of_rows
|
|
432
|
+
|
|
433
|
+
row_to : int
|
|
434
|
+
row to end search (default number_of_rows)
|
|
435
|
+
|
|
436
|
+
should be between 1 and number_of_rows
|
|
437
|
+
|
|
438
|
+
column1 : int
|
|
439
|
+
column to search in (default 1)
|
|
440
|
+
|
|
441
|
+
should be between 1 and number_of_columns
|
|
442
|
+
|
|
443
|
+
column2 : int
|
|
444
|
+
column to return looked up value from (default column1 + 1)
|
|
445
|
+
|
|
446
|
+
should be between 1 and number_of_columns
|
|
447
|
+
|
|
448
|
+
Returns
|
|
449
|
+
-------
|
|
450
|
+
block[found row number, column2] : any
|
|
451
|
+
|
|
452
|
+
Note
|
|
453
|
+
----
|
|
454
|
+
If s is not found, a ValueError is raised
|
|
455
|
+
"""
|
|
388
456
|
if column2 is None:
|
|
389
457
|
column2 = column1 + 1
|
|
390
458
|
self._check_column(column2, "column2")
|
|
@@ -392,6 +460,40 @@ class block:
|
|
|
392
460
|
return self[row, column2]
|
|
393
461
|
|
|
394
462
|
def lookup_row(self, s, *, row_from=1, row_to=None, column1=1):
|
|
463
|
+
"""
|
|
464
|
+
searches in column1 for row between row_from and row_to for s and returns that row number
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
s : any
|
|
469
|
+
value to seach for
|
|
470
|
+
|
|
471
|
+
row_from : int
|
|
472
|
+
row to start search (default 1)
|
|
473
|
+
|
|
474
|
+
should be between 1 and number_of_rows
|
|
475
|
+
|
|
476
|
+
row_to : int
|
|
477
|
+
row to end search (default number_of_rows)
|
|
478
|
+
|
|
479
|
+
should be between 1 and number_of_rows
|
|
480
|
+
|
|
481
|
+
column1 : int
|
|
482
|
+
column to search in (default 1)
|
|
483
|
+
|
|
484
|
+
should be between 1 and number_of_columns
|
|
485
|
+
|
|
486
|
+
column2 : int
|
|
487
|
+
column to return looked up value from (default column1 + 1)
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
row number where block[row nunber, column1] == s : int
|
|
492
|
+
|
|
493
|
+
Note
|
|
494
|
+
----
|
|
495
|
+
If s is not found, a ValueError is raised
|
|
496
|
+
"""
|
|
395
497
|
if row_to is None:
|
|
396
498
|
row_to = self.highest_used_row_number
|
|
397
499
|
self._check_row(row_from, "row_from")
|
|
@@ -404,6 +506,42 @@ class block:
|
|
|
404
506
|
raise ValueError(f"{s} not found")
|
|
405
507
|
|
|
406
508
|
def hlookup(self, s, *, column_from=1, column_to=None, row1=1, row2=None):
|
|
509
|
+
"""
|
|
510
|
+
searches in row1 for column between column_from and column_to for s and returns the value found at (that column, row2)
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
s : any
|
|
515
|
+
value to seach for
|
|
516
|
+
|
|
517
|
+
column_from : int
|
|
518
|
+
column to start search (default 1)
|
|
519
|
+
|
|
520
|
+
should be between 1 and number_of_columns
|
|
521
|
+
|
|
522
|
+
column_to : int
|
|
523
|
+
column to end search (default number_of_columns)
|
|
524
|
+
|
|
525
|
+
should be between 1 and number_of_columns
|
|
526
|
+
|
|
527
|
+
row1 : int
|
|
528
|
+
row to search in (default 1)
|
|
529
|
+
|
|
530
|
+
should be between 1 and number_of_rows
|
|
531
|
+
|
|
532
|
+
row2 : int
|
|
533
|
+
row to return looked up value from (default row1 + 1)
|
|
534
|
+
|
|
535
|
+
should be between 1 and number_of_rows
|
|
536
|
+
|
|
537
|
+
Returns
|
|
538
|
+
-------
|
|
539
|
+
block[row, found column, row2] : any
|
|
540
|
+
|
|
541
|
+
Note
|
|
542
|
+
----
|
|
543
|
+
If s is not found, a ValueError is raised
|
|
544
|
+
"""
|
|
407
545
|
if row2 is None:
|
|
408
546
|
row2 = row1 + 1
|
|
409
547
|
self._check_row(row2, "row2")
|
|
@@ -411,6 +549,40 @@ class block:
|
|
|
411
549
|
return self[row2, column]
|
|
412
550
|
|
|
413
551
|
def lookup_column(self, s, *, column_from=1, column_to=None, row1=1):
|
|
552
|
+
"""
|
|
553
|
+
searches in row1 for column between column_from and column_to for s and returns that column number
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
s : any
|
|
558
|
+
value to seach for
|
|
559
|
+
|
|
560
|
+
column_from : int
|
|
561
|
+
column to start search (default 1)
|
|
562
|
+
|
|
563
|
+
should be between 1 and number_of_columns
|
|
564
|
+
|
|
565
|
+
column_to : int
|
|
566
|
+
column to end search (default number_of_columns)
|
|
567
|
+
|
|
568
|
+
should be between 1 and number_of_columns
|
|
569
|
+
|
|
570
|
+
row1 : int
|
|
571
|
+
row to search in (default 1)
|
|
572
|
+
|
|
573
|
+
should be between 1 and number_of_rows
|
|
574
|
+
|
|
575
|
+
row2 : int
|
|
576
|
+
row to return looked up value from (default row1 + 1)
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
column number where block[row1, column number] == s : int
|
|
581
|
+
|
|
582
|
+
Note
|
|
583
|
+
----
|
|
584
|
+
If s is not found, a ValueError is raised
|
|
585
|
+
"""
|
|
414
586
|
if column_to is None:
|
|
415
587
|
column_to = self.highest_used_column_number
|
|
416
588
|
self._check_column(column_from, "column_from")
|
|
@@ -423,6 +595,44 @@ class block:
|
|
|
423
595
|
raise ValueError(f"{s} not found")
|
|
424
596
|
|
|
425
597
|
def lookup(self, s, *, row_from=1, row_to=None, column1=1, column2=None):
|
|
598
|
+
"""
|
|
599
|
+
searches in column1 for row between row_from and row_to for s and returns the value found at (that row, column2)
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
s : any
|
|
604
|
+
value to seach for
|
|
605
|
+
|
|
606
|
+
row_from : int
|
|
607
|
+
row to start search (default 1)
|
|
608
|
+
|
|
609
|
+
should be between 1 and number_of_rows
|
|
610
|
+
|
|
611
|
+
row_to : int
|
|
612
|
+
row to end search (default number_of_rows)
|
|
613
|
+
|
|
614
|
+
should be between 1 and number_of_rows
|
|
615
|
+
|
|
616
|
+
column1 : int
|
|
617
|
+
column to search in (default 1)
|
|
618
|
+
|
|
619
|
+
should be between 1 and number_of_columns
|
|
620
|
+
|
|
621
|
+
column2 : int
|
|
622
|
+
column to return looked up value from (default column1 + 1)
|
|
623
|
+
|
|
624
|
+
should be between 1 and number_of_columns
|
|
625
|
+
|
|
626
|
+
Returns
|
|
627
|
+
-------
|
|
628
|
+
block[found row number, column2] : any
|
|
629
|
+
|
|
630
|
+
Note
|
|
631
|
+
----
|
|
632
|
+
If s is not found, a ValueError is raised
|
|
633
|
+
|
|
634
|
+
This is exactly the same as vlookup.
|
|
635
|
+
"""
|
|
426
636
|
return self.vlookup(s, row_from=row_from, row_to=row_to, column1=column1, column2=column2)
|
|
427
637
|
|
|
428
638
|
|
|
@@ -451,10 +661,10 @@ class _capture:
|
|
|
451
661
|
self._buffer = []
|
|
452
662
|
|
|
453
663
|
def __enter__(self):
|
|
454
|
-
|
|
664
|
+
self.enabled = True
|
|
455
665
|
|
|
456
666
|
def __exit__(self, exc_type, exc_value, tb):
|
|
457
|
-
|
|
667
|
+
self.enabled = False
|
|
458
668
|
|
|
459
669
|
def write(self, data):
|
|
460
670
|
self._buffer.append(data)
|
|
@@ -466,6 +676,17 @@ class _capture:
|
|
|
466
676
|
self.stdout.flush()
|
|
467
677
|
self._buffer.append("\n")
|
|
468
678
|
|
|
679
|
+
@property
|
|
680
|
+
def enabled(self):
|
|
681
|
+
return sys.out == self
|
|
682
|
+
|
|
683
|
+
@enabled.setter
|
|
684
|
+
def enabled(self, value):
|
|
685
|
+
if value:
|
|
686
|
+
sys.stdout = self
|
|
687
|
+
else:
|
|
688
|
+
sys.stdout = self.stdout
|
|
689
|
+
|
|
469
690
|
@property
|
|
470
691
|
def value(self):
|
|
471
692
|
result = self.value_keep
|
|
@@ -501,12 +722,14 @@ class _capture:
|
|
|
501
722
|
|
|
502
723
|
|
|
503
724
|
def reset():
|
|
725
|
+
capture.enabled = False
|
|
504
726
|
capture.include_print = False
|
|
505
727
|
capture.clear()
|
|
506
|
-
|
|
728
|
+
print('reset',sys.stdout)
|
|
507
729
|
|
|
508
730
|
capture = _capture()
|
|
509
731
|
|
|
510
732
|
|
|
511
733
|
if __name__ == "__main__":
|
|
512
734
|
...
|
|
735
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xlwings_utils
|
|
3
|
-
Version: 25.0.0.
|
|
3
|
+
Version: 25.0.0.post3
|
|
4
4
|
Summary: xlwings_utils
|
|
5
5
|
Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/salabim/xlwings_utils
|
|
@@ -119,13 +119,80 @@ sheet.range(10,1).value = this_block.minimized().value
|
|
|
119
119
|
|
|
120
120
|
In this case, only the really processed rows are copied to the sheet.
|
|
121
121
|
|
|
122
|
+
### Looking up in a block
|
|
123
|
+
|
|
124
|
+
With blocks, it is easy to use a sheet as an input for a project / scenario.
|
|
125
|
+
|
|
126
|
+
Something like
|
|
127
|
+
<img src="https://www.salabim.org/xlwings_utils/fig01.png">
|
|
128
|
+
|
|
129
|
+
Of course, we could access the various input fields with absolute ranges, but if something changes later (like adding a row), all references would have to be updated.
|
|
130
|
+
|
|
131
|
+
If we read the project sheet (partly) into a block, lookup methods are available to access 'fields' easily and future-proof.
|
|
132
|
+
|
|
133
|
+
Let's see how this works with the above sheet. The corresponding block (bl) looks like
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
| 1 2 3 4 5
|
|
137
|
+
--+-------------------------------------------------------------------------------
|
|
138
|
+
1 | Project Factory1
|
|
139
|
+
2 | Start date 2025-05-17
|
|
140
|
+
3 | End date 2026-02-01
|
|
141
|
+
4 |
|
|
142
|
+
5 | Parts Width Length Height Weight
|
|
143
|
+
6 | A 10 5 5 100
|
|
144
|
+
7 | B 11 5 8 102
|
|
145
|
+
8 | C 12 2 3 91
|
|
146
|
+
9 |
|
|
147
|
+
```
|
|
148
|
+
Now we can do
|
|
149
|
+
```project = bl.lookup("Project")
|
|
150
|
+
project = bl.lookup("Project")
|
|
151
|
+
start_date = bl.lookup("Start date")
|
|
152
|
+
end_date = bl.lookup("End date")
|
|
153
|
+
row1 = bl.lookup_row("Parts")
|
|
154
|
+
parts=[]
|
|
155
|
+
for row2 in range(row1 + 1, bl.highest_used_row_number + 1):
|
|
156
|
+
if not (part_name := bl.hlookup("Part",row1=row1, row2=row2)):
|
|
157
|
+
# stop when a 'blank' part_name is found
|
|
158
|
+
break
|
|
159
|
+
width = bl.hlookup("Width",row1=row1, row2=row2)
|
|
160
|
+
length = bl.hlookup("Length",row1=row1, row2=row2)
|
|
161
|
+
height = bl.hlookup("HeightL",row1=row1, row2=row2)
|
|
162
|
+
weight = bl.hlookup("Weight",row1=row1, row2=row2)
|
|
163
|
+
parts.append(Part(part_name, width, length, height, weight))
|
|
164
|
+
```
|
|
165
|
+
First we do a couple of `lookup`s, which are vertical lookups, to scan column 1 for the given labels and return the corresponding values from column 2.
|
|
166
|
+
|
|
167
|
+
Then, there's `lookup_row`, which also scans column1 for the given label (Parts), but returns the corresponding row (5). It is then stored in row1.
|
|
168
|
+
And then we just read the following rows (with `hlookup`) and access the required values.
|
|
169
|
+
|
|
170
|
+
### Filling a block from other sources
|
|
171
|
+
|
|
172
|
+
The advantage of using a block instead of accessing these sources is, that they are one-based, just like in Excel.
|
|
173
|
+
|
|
174
|
+
It is possible to make a block from a xlrd worksheet with `block.from_xlrd_sheet`.
|
|
175
|
+
|
|
176
|
+
It is possible to make a block from a pandas dataframe with `block.from_dataframe`. Make sure that, if the dataframe is created by reading from an Excel sheet, headers=None should be specified, e.g. `df = pd.read_excel(filename, header=None)`.
|
|
177
|
+
|
|
178
|
+
It is possible to make a block from an openpyxl worksheet with `block.from_openpyxl_sheet`.
|
|
179
|
+
|
|
180
|
+
### Writing a block to an openpyxl sheet
|
|
181
|
+
|
|
182
|
+
In order to write (append) to an openpyxl sheet, use: block.to_openpyxl_sheet.
|
|
183
|
+
|
|
184
|
+
It is possible to make a block from a text file with `block.from_file`.
|
|
185
|
+
|
|
186
|
+
### Writing a block to an openpyxl sheet
|
|
187
|
+
|
|
122
188
|
## Capture stdout support
|
|
123
189
|
|
|
124
190
|
The module has support for capturing stdout and -later- using showing the captured output on a sheet.
|
|
125
191
|
|
|
126
192
|
This is rather important as printing in xlwings lite to the UI pane is rather slow.
|
|
127
193
|
|
|
128
|
-
In order to capture stdout output, use
|
|
194
|
+
In order to capture stdout output, use `xwu.capture.enabled = True`. And to stop capturing, use `xwu.capture.enabled = False`.
|
|
195
|
+
Alternatively, a context manager is provided:
|
|
129
196
|
|
|
130
197
|
|
|
131
198
|
```
|
|
@@ -134,8 +201,9 @@ with xwu.capture:
|
|
|
134
201
|
code with print statements
|
|
135
202
|
"""
|
|
136
203
|
```
|
|
204
|
+
Note that stopping the capture, leaves the captured output in place, so it can be extended later.
|
|
137
205
|
|
|
138
|
-
|
|
206
|
+
In either case, the captured output can be then copied to a sheet, like
|
|
139
207
|
|
|
140
208
|
```
|
|
141
209
|
sheet.range(4,5).value = xwu.capture.value
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
xlwings_utils/__init__.py,sha256=FdaRztevSu5akGL7KBUBRzqwLMRTdvVUuS2Kfp2f1Uc,68
|
|
2
|
+
xlwings_utils/xlwings_utils.py,sha256=o1iCbb5BmXDeVelksKNTJrSkGZ7e0y-iwnYlITKr3dQ,21039
|
|
3
|
+
xlwings_utils-25.0.0.post3.dist-info/METADATA,sha256=Mwx61halKRiHZAvXI5eoSrLiV5lqUCwKVkZu-tZ3P5E,9537
|
|
4
|
+
xlwings_utils-25.0.0.post3.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
|
5
|
+
xlwings_utils-25.0.0.post3.dist-info/top_level.txt,sha256=kf5SEv0gZiRObPhUoYcc1O_iX_wwTOPeUIYvzyYeAM4,14
|
|
6
|
+
xlwings_utils-25.0.0.post3.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
xlwings_utils/__init__.py,sha256=FdaRztevSu5akGL7KBUBRzqwLMRTdvVUuS2Kfp2f1Uc,68
|
|
2
|
-
xlwings_utils/xlwings_utils.py,sha256=UQenav-IEe7Hl8IQKNVLYDBscEdmvkR4IQbptsq3Obg,14830
|
|
3
|
-
xlwings_utils-25.0.0.post1.dist-info/METADATA,sha256=Vh86pfRnT_0wuFKqO643CMLViq2YdcqAGoaNx53eEiE,6166
|
|
4
|
-
xlwings_utils-25.0.0.post1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
5
|
-
xlwings_utils-25.0.0.post1.dist-info/top_level.txt,sha256=kf5SEv0gZiRObPhUoYcc1O_iX_wwTOPeUIYvzyYeAM4,14
|
|
6
|
-
xlwings_utils-25.0.0.post1.dist-info/RECORD,,
|
|
File without changes
|