lib.helpers
1from collections import UserList 2from collections.abc import MappingView, Sequence 3from typing import Any, Literal, TypeVar, TypeAlias, Generator 4import sys 5import random 6from math import ceil 7from itertools import groupby 8from copy import deepcopy 9from itertools import islice 10import loguru 11from icecream import ic 12 13T = TypeVar("T") 14Strategy: TypeAlias = Literal["pick", "omit"] 15"""Strategy for filtering items in a list.""" 16 17 18def setLogLevel( 19 instance: "loguru.Logger", 20 level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG", 21): 22 """Set the log level and format for a loguru.Logger instance. 23 24 Args: 25 instance: The loguru.Logger instance to configure. 26 level: The log level to set. 27 """ 28 instance.remove() 29 instance.add( 30 sink=sys.stderr, 31 level=level.upper(), 32 format="<level>{level: ^8}</level> <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> <level>{message}</level>", 33 ) 34 35 36# Numbers 37# region 38def isInRange(start: int, end: int, value: int): 39 """Returns True if value is in the inclusive range [start, end].""" 40 return start <= value <= end 41 42 43# endregion 44 45 46# Strings 47# region 48def truncateString(string: str, length=10, suffix="...") -> str: 49 """ 50 Truncate a string to a given length and append a suffix. 51 52 Example: 53 `(5)` `Hello there!` => `Hello...` 54 """ 55 return string[:length] + suffix 56 57 58# endregion 59 60 61# Iterables 62# region 63def coerceList(value, separate=False, strict=False) -> list: 64 """Coerce a value to a list. 65 66 Args: 67 value: The value to coerce. 68 separate: If True, split str to characters. 69 - (False) `"abc"` => `["abc"]` 70 - (True) `"abc"` => `["a", "b", "c"]` 71 strict: If True, force tuple to list. 72 - (False) `(a, b)` => `(a, b)` 73 - (True) `(a, b)` => `[(a, b)]` 74 75 Returns: 76 The coerced list. 77 78 Example: 79 - `1` or `[1]` => `[1]` 80 - `1, 2` => `[1, 2]` 81 """ 82 passingTypes = list if strict else (list, tuple, UserList) 83 # Check if not None, can be [] 84 if value is not None: 85 if isinstance(value, passingTypes): 86 return value 87 # Handle mapping views, e.g. dict.keys(), dict.values() 88 elif isinstance(value, MappingView): 89 return list(value) 90 elif isSequence(value): 91 return [value] 92 else: 93 return list(value) if separate else [value] 94 95 96def coerceNumericList(value, separator=" ") -> list[int]: 97 """Coerce a value to a list of integers. 98 99 Args: 100 value: The value to coerce. 101 separator: Separator for splitting strings. 102 103 Returns: 104 List of integers. 105 106 Example: 107 - (int) `5` => `[5]` 108 - (str) `"5mm"` => `["5mm"]` 109 - (str) `"5 5"` => `[5, 5]` 110 """ 111 if isSequence(value): 112 return value 113 114 if isinstance(value, str): 115 if separator in value: 116 parts = value.split(separator) 117 try: 118 return [int(side) for side in parts] 119 except ValueError: 120 return parts 121 try: 122 return [int(value)] 123 except ValueError: 124 pass 125 126 return [value] 127 128 129def coerceTuple(value) -> tuple: 130 """Coerce a value to a tuple. 131 132 Args: 133 value: The value to coerce. 134 135 Returns: 136 The coerced tuple. 137 138 Example: 139 - `1` or `(1,)` => `(1,)` 140 - `1, 2` => `(1, 2)` 141 """ 142 if isinstance(value, tuple): 143 return value 144 elif value is not None: 145 if isinstance(value, list): 146 return tuple(value) 147 if isinstance(value, str) and " " in value: 148 return tuple(value.split()) 149 return (value,) 150 151 152def expand( 153 input, n=2, defaultValue=None, format: Literal["auto", "list", "tuple"] = "auto" 154) -> list | tuple: 155 """Expand to iterable with length of `n`. 156 157 Args: 158 input: The input value(s) to expand. 159 n: Desired length. 160 defaultValue: Fallback value for cycling. 161 format: Output type. 162 163 Returns: 164 Iterable of length n. 165 166 Example: 167 - `n=3`: `a` => `[a, a, a]` 168 - `n=4`: `[1, 2]` => `[1, 2, 1, 2]` 169 """ 170 171 def _getValueAtIndex(i: int): 172 try: 173 return input[i] 174 except IndexError: 175 return defaultValue if defaultValue is not None else input[i % len(input)] 176 177 input = coerceList(input) 178 if format == "auto": 179 outputFormat = tuple if isinstance(input, tuple) else list 180 else: 181 outputFormat = tuple if format == "tuple" else list 182 return outputFormat([_getValueAtIndex(i) for i in range(n)]) 183 184 185def reverseList(input: list[T]) -> list[T]: 186 """Inverse values in iterable. 187 188 Returns: 189 The reversed list. 190 """ 191 return input[::-1] 192 193 194def previewList(input: list, length=3) -> str: 195 """ 196 Get a preview of the list with a specified number of items and a count of remaining items. 197 198 Example: 199 (2) `[A, B, C, D]` => `[A, B] and 2 more items` 200 """ 201 remainder = len(input) - length 202 noun = "items" if remainder > 1 else "item" 203 204 output = f"{input[:length]}" 205 206 if remainder > 0: 207 output += f" and {remainder} more {noun}" 208 209 return output 210 211 212def sampleList(input: list[T], size: int, shuffle=True) -> list[T]: 213 """Get random sample of specified size from list. 214 215 Args: 216 input: List to sample from. 217 size: Number of items to sample. 218 shuffle: If True, shuffle the list before sampling. 219 220 Returns: 221 Random sample of specified size. 222 """ 223 if shuffle: 224 return random.sample(input, min(size, len(input))) 225 return input[:size] 226 227 228def inverseValues(values: list[Any]) -> list[Any]: 229 """Negate all values in a list. 230 231 Example: 232 - `[1, 2]` => `[-1, -2]` 233 - ["a", "b"]` => `["-a", "-b"]` 234 """ 235 236 def _negate(value): 237 if isinstance(value, (int, float)): 238 return -value 239 elif isinstance(value, str): 240 return f"-{value}" 241 else: 242 raise TypeError(f"Unsupported type for negation: {type(value).__name__}") 243 244 return [_negate(value) for value in coerceList(values)] 245 246 247def flatten(items: list): 248 """Flatten a list of lists or tuples. 249 250 Returns: 251 Flattened list or original input if not all items are lists/tuples. 252 """ 253 areItemsLists = all([isinstance(item, list) for item in items]) 254 areItemsTuples = all([isinstance(item, tuple) for item in items]) 255 256 if areItemsLists or areItemsTuples: 257 start = [] if areItemsLists else () 258 return list(sum(items, start)) 259 # Otherwise pass through unchanged 260 else: 261 isIterable = isinstance(items, (list, tuple)) 262 if not isIterable: 263 loguru.logger.debug("Unable to flatten: {}", items) 264 return items 265 266 267def isFlat(items: list) -> bool: 268 """Returns True if all items in the list are not lists or tuples. 269 270 Example: 271 `[1, 2, 3]` => `True` 272 `[[1, 2], 3]` => `False` 273 """ 274 return all([not isinstance(item, (list, tuple)) for item in items]) 275 276 277def flattenTuples(input): 278 """Flatten nested tuples and lists into a single list of tuples. 279 280 Example: 281 `[(1, 2), [(3, 4), (5, 6)]]` => `[(1, 2), (3, 4), (5, 6)]` 282 """ 283 output = [] 284 285 if isinstance(input, tuple): 286 input = [input] 287 288 for item in input: 289 if isinstance(item, list): 290 for subitem in item: 291 output.append(subitem) 292 else: 293 output.append(item) 294 return output 295 296 297def intersect(lists: list[list], retainOrder=True) -> list: 298 """Return intersection of multiple lists. 299 300 Args: 301 lists: List of lists to intersect. 302 retainOrder: If True, order based on first list. 303 304 Returns: 305 List of intersected items. 306 307 Example: 308 `[a, b, c]` + `[b]` => `[b]` 309 """ 310 result = set.intersection(*map(set, lists)) 311 listA = lists[0] 312 313 return sorted(result, key=lambda x: listA.index(x)) if retainOrder else list(result) 314 315 316def zipWithMode( 317 groups: list[list], mode: Literal["drop", "fill"] = "fill" 318) -> list[list]: 319 """Zip lists with different modes. 320 321 Args: 322 groups: List of lists to zip. 323 mode: 'drop' to zip to shortest, 'fill' to zip to longest. 324 325 Returns: 326 Zipped list of lists. 327 """ 328 329 def _fillGroup(group): 330 isShorter = len(group) < longest 331 return [group[i % len(group)] for i in range(longest)] if isShorter else group 332 333 if mode == "fill": 334 groups = [group if group else [group] for group in groups] # None => [None] 335 longest = max([len(group) for group in groups]) 336 groups = [_fillGroup(group) for group in groups] 337 338 return list(zip(*groups)) 339 340 341def removeFalsy( 342 input: list, mode: Literal[None, False, "empty", "either"] = "either" 343) -> list: 344 """Remove falsy values from a list. 345 346 Args: 347 input: List to filter. 348 mode: 'None', 'False', 'empty', or 'either'. 349 350 Returns: 351 Filtered list. 352 """ 353 354 def _checkItem(item): 355 isNotNone = item is not None 356 isNotFalse = item is not False 357 isNotEmpty = isinstance(item, list) and len(item) 358 isNotEither = isNotNone and isNotFalse and isNotEmpty 359 if mode is None: 360 return isNotNone 361 elif mode is False: 362 return isNotFalse 363 else: 364 return isNotEither 365 366 return list(filter(_checkItem, input)) 367 368 369def removeNone(input: list) -> list: 370 """Remove `None` only from a list. 371 372 Returns: 373 List with None values removed. 374 """ 375 return removeFalsy(input=input, mode=None) 376 377 378def removeFromList(input: list, remove: list) -> list: 379 """Remove specified items from a list. 380 381 Example: 382 `[a, b, c]` + `[b]` => `[a, c]` 383 384 Returns: 385 List with specified items removed. 386 """ 387 output = [] 388 remove = flatten(coerceList(remove)) 389 390 for element in input: 391 if element not in remove: 392 output.append(element) 393 394 return output 395 396 397def dedupe( 398 input: list[str], sortAlphabetically=False, caseSensitive=False, debug=False 399): 400 """Remove duplicates in list (= uniq). 401 402 Args: 403 input: List to deduplicate. 404 sortAlphabetically: If True, sort output. 405 caseSensitive: If False, dedupe case-insensitively. 406 debug: If True, log duplicates. 407 408 Returns: 409 Deduplicated list. 410 """ 411 input = [item for item in input if item] # Remove None 412 output = list() 413 uniq = set() 414 415 for item in input: 416 itemCase = ( 417 item if caseSensitive or isinstance(item, (int, float)) else item.casefold() 418 ) 419 if itemCase not in uniq: 420 uniq.add(itemCase) 421 output.append(item) 422 elif debug: 423 loguru.logger.trace("[Dedupe] {}", item) 424 425 if sortAlphabetically: 426 output = sorted(output) 427 428 return output 429 430 431def findClosestValue(values: list, value: int, sort=True, discardLarger=False) -> int: 432 """Return closest value in a sorted list. 433 434 If two values are equally close, return the smallest value. 435 436 Args: 437 values: List of values. 438 value: Value to compare. 439 sort: If True, sort the list. 440 discardLarger: If True, discard values larger than input. 441 442 Returns: 443 Closest value or None if list is empty. 444 """ 445 from bisect import bisect_left 446 447 # Discard larger values 448 if discardLarger: 449 values = [v for v in values if v <= value] 450 451 if sort: 452 values = sorted(values) 453 454 pos = bisect_left(values, value) 455 if pos == 0: 456 return values[0] if len(values) else None 457 if pos == len(values): 458 return values[-1] 459 before = values[pos - 1] 460 after = values[pos] 461 462 if after - value < value - before: 463 return after 464 else: 465 return before 466 467 468def findClosestIndex(values: list, value: str) -> int: 469 """Find index of closest matching string in a list. 470 471 Useful for finding `sort()` key. 472 473 Example: 474 `["B", "F"], "Foo"` => returns `1` 475 476 Returns: 477 Index of closest match or None. 478 """ 479 if not value: 480 return None 481 482 indexFound = None 483 484 while len(value) and not isinstance(indexFound, int): 485 try: 486 indexFound = values.index(value) 487 except ValueError: 488 pass 489 490 value = value[:-1] 491 492 return indexFound 493 494 495def isSequence(value) -> bool: 496 """Check if value is a sequence (list or tuple) but not a string.""" 497 return isinstance(value, Sequence) and not isinstance(value, (str, bytes)) 498 499 500def isListOfOne(value) -> bool: 501 """Returns True if the list contains exactly one item. 502 503 Example: 504 - `[1, 2]` => `false` 505 - `1` => `false` 506 - `[1]` => `true` 507 """ 508 return isSequence(value) and len(value) == 1 509 510 511def isFirst( 512 parent: list | int, 513 child: list | int, 514 mode: Literal["indices", "lists"] = "indices", 515): 516 """Returns True if child is the first element in parent. 517 518 Args: 519 parent: Iterable or count. 520 child: Item or index. 521 mode: 'indices' or 'lists'. 522 """ 523 childIndex = ( 524 child if isinstance(child, int) and mode == "indices" else parent.index(child) 525 ) 526 return childIndex == 0 527 528 529def isLast( 530 parent: list | int, 531 child: list | int, 532 mode: Literal["indices", "lists"] = "indices", 533): 534 """Returns True if child is the last element in parent. 535 536 Args: 537 parent: Iterable or count. 538 child: Item or index. 539 mode: 'indices' or 'lists'. 540 """ 541 childIndex = ( 542 child if isinstance(child, int) and mode == "indices" else parent.index(child) 543 ) 544 parentCount = len(parent) if not isinstance(parent, int) else parent 545 return childIndex == parentCount - 1 546 547 548def isNth(index: int, nth: int = 3): 549 """Returns True if index is every nth element. 550 551 Args: 552 index: Index to check. 553 nth: Nth interval. 554 555 Example: 556 `nth=3` => Every third 557 """ 558 return (index + 1) % nth == 0 559 560 561def pickFirst(value): 562 """Get first value from iterable if iterable. 563 564 Returns: 565 First value or value itself if not iterable. 566 """ 567 if isSequence(value): 568 return value[0] 569 else: 570 return value 571 572 573def pickLast(value): 574 """Get last value from iterable if iterable. 575 576 Returns: 577 Last value or value itself if not iterable. 578 """ 579 if isSequence(value): 580 return value[-1] 581 else: 582 return value 583 584 585def pickRandom(items: list) -> tuple: 586 """Pick 1 item at random and return remaining items. 587 588 Returns: 589 Tuple of (item, remainingItems). 590 """ 591 item = random.choice(items) 592 itemIndex = items.index(item) 593 items.pop(itemIndex) 594 595 return item, items 596 597 598def pickExtremes(values: list): 599 """Get first and last from list. 600 601 Example: 602 `[1, 2, 3, 4]` => `(1, 4)` 603 604 Returns: 605 Tuple of (first, last) or values itself if length <= 1. 606 """ 607 if len(values) > 1: 608 return values[0], values[-1] 609 else: 610 return values 611 612 613def pushToEnd(items: list[T], index=0): 614 """Move item at index to the end and return modified list. 615 616 Example: 617 `ABCD` `index=0` => `BCDA` 618 619 Returns: 620 Modified list. 621 """ 622 result = items.copy() # Avoid mutating original 623 result.append(result.pop(index)) 624 return result 625 626 627def _splitToGroups(input: list | str, groups=2) -> Generator: 628 """Split input into groups of approximately equal size. 629 630 Args: 631 input: List or string to split. 632 groups: Number of groups. 633 634 Yields: 635 Groups as tuples or strings. 636 """ 637 it = iter(input) 638 while batch := tuple(islice(it, ceil(len(input) / groups))): 639 yield "".join(batch) if isinstance(input, str) else batch 640 641 642def halven(input: T) -> list[T]: 643 """Split input into two halves. Works for lists and strings. 644 645 - `even`: two equal halves 646 - `odd`: larger/smaller half 647 648 Returns: 649 List of two halves (even or odd sized). 650 """ 651 return list(_splitToGroups(input, 2)) 652 653 654def splitEvenly(input: T, groups: int = 3) -> list[T]: 655 """Split input into evenly sized groups. Works for lists and strings. 656 657 Args: 658 input: List or string to split. 659 groups: Number of groups. 660 661 Example: 662 `groups=2`, `ABCD` => `AB CD` 663 664 Returns: 665 List of groups. 666 """ 667 return list(_splitToGroups(input, groups)) 668 669 670def toChunks(items: list[T], groups=2) -> list[list[T]]: 671 """Combine groups in alternating manner. 672 673 Example: 674 `groups=2`, `ABCD` => `AC BD` 675 676 Returns: 677 List of chunked groups. 678 """ 679 return [items[start::groups] for start in range(groups)] 680 681 682def shuffleAtRandomSegment(items: list[T]) -> list[T]: 683 """Shuffle list at a random segment to maintain order. 684 685 Returns: 686 List shuffled at a random index. 687 """ 688 randomItem = random.choice(items) 689 randomIndex = items.index(randomItem) 690 return flatten([items[randomIndex:], items[:randomIndex]]) 691 692 693def shuffleAtInterval(items: list[T], n: int = 2) -> list[T]: 694 """ 695 Rearrange a list by grouping every nth item together, then flattening the result. 696 697 Example: 698 - For items `[A, B, C, D, E, F]` and n `3`: 699 - The list is split into 3 groups: `[A, D], [B, E], [C, F]` 700 - The result is: `[A, D, B, E, C, F]` 701 702 Returns: 703 A new list with items reordered by interval. 704 """ 705 return sum(toChunks(items, n), []) 706 707 708def spaceEvenly(items: list, size=2): 709 """Get n items from list, evenly spaced. 710 711 Args: 712 items: List of items. 713 size: Number of items to get. 714 715 Example: 716 - 1 => get midpoint 717 - 2 => get first and last 718 719 Returns: 720 List of evenly spaced items. 721 """ 722 723 def _getSpacedIndices(start, stop, length): 724 return [round(start + x * (stop - start) / (length - 1)) for x in range(length)] 725 726 itemCount = len(items) 727 if size == 1: 728 # Return midpoint 729 indices = [len(items) >> 1] 730 else: 731 # Never return size larger than itemCount 732 indices = _getSpacedIndices(0, itemCount - 1, min(itemCount, size)) 733 734 return [items[i] for i in indices] 735 736 737# endregion 738 739 740# Object: dict 741# region 742def pick(object: dict, keys: list[str]) -> dict: 743 """Pick properties by `key`. 744 745 Args: 746 object: Dictionary to pick from. 747 keys: Keys to pick. 748 749 Returns: 750 Dictionary with only picked keys. 751 """ 752 return {k: v for k, v in object.items() if k in coerceList(keys)} 753 754 755def omit(object: dict, keys: list[str]) -> dict: 756 """Remove properties by `key`. 757 758 Args: 759 object: Dictionary to omit from. 760 keys: Keys to omit. 761 762 Returns: 763 Dictionary with omitted keys. 764 """ 765 return {k: v for k, v in object.items() if k not in coerceList(keys)} 766 767 768def omitBy(object: dict, omit=[0, None, []]) -> dict: 769 """Remove properties by `value`. 770 771 Remove falsy by default: `0`, `None`, `[]`. 772 773 Args: 774 object: Dictionary to filter. 775 omit: Values to omit. 776 777 Returns: 778 Dictionary with omitted values. 779 """ 780 return {k: v for k, v in object.items() if v not in coerceList(omit)} 781 782 783# endregion 784 785 786# Collections: list[dict] 787# region 788def merge(dicts: list[dict]): 789 """Merge multiple instances of `dict`. 790 791 What happens to values: 792 - `str` gets overwritten by last occurrence 793 - `list` gets extended (`flatten` + `dedupe`) 794 795 Args: 796 dicts: List of dictionaries to merge. 797 798 Returns: 799 Merged dictionary. 800 """ 801 result = dict() 802 803 for d in dicts: 804 for [key, value] in d.items(): 805 hasKey = result.get(key) 806 if hasKey: 807 if isinstance(value, list): 808 result[key] = dedupe(flatten([result[key], value])) 809 else: 810 result[key] = value 811 else: 812 result[key] = value 813 814 return result 815 816 817def findByKey(items: list[dict], key: str, value, defaultValue=None) -> dict | None: 818 """Find item in list of dictionaries/objects by key. 819 820 Args: 821 items: List of dictionaries or objects. 822 key: Key to search for. 823 value: Value to match. 824 defaultValue: Value to return if not found. 825 826 Returns: 827 The found item or defaultValue. 828 """ 829 830 def getValue(item, key): 831 # Use built-in method for dictionaries 832 if isinstance(item, dict): 833 return item.get(key) 834 # Otherwise assume it’s an object 835 else: 836 return getattr(item, key) 837 838 return next((item for item in items if getValue(item, key) == value), defaultValue) 839 840 841def sortByKey(items: list[dict], key: str, defaultValue: int | str = None): 842 """Sort list of dictionaries by key. 843 844 Args: 845 items: List of dictionaries. 846 key: Key to sort by. 847 defaultValue: Value to use if key is missing. 848 849 Returns: 850 Sorted list or original if sorting fails. 851 """ 852 # May be [] 853 try: 854 return sorted(items, key=lambda item: item.get(key, defaultValue)) 855 except TypeError: 856 return items 857 858 859def groupByKey(items: list[dict], key: str) -> list[list[dict]]: 860 """Group list of dictionaries by `key`. 861 862 Args: 863 items: List of dictionaries. 864 key: Key to group by. 865 866 Returns: 867 Grouped items as an iterator. 868 """ 869 return groupby(items, key=lambda item: item.get(key)) 870 871 872def reject(items: list[dict], key: str, value): 873 """Remove items matching `key`:`value`. 874 875 Args: 876 items: List of dictionaries. 877 key: Key to check. 878 value: Value to reject. 879 880 Returns: 881 Filtered list. 882 """ 883 return [item for item in items if item.get(key) != value] 884 885 886def mergeCollections( 887 main: list[dict], 888 additions: list[dict], 889 key: str = "id", 890 caseSensitive=True, 891 mode: Literal["merge", "replace"] = "merge", 892) -> list[dict]: 893 """Merge two instances of `list[dict]` and dedupe by key. 894 895 Args: 896 main: Main list of dictionaries. 897 additions: List of dictionaries to merge in. 898 key: Key to match for merging. 899 caseSensitive: If False, dedupe case-insensitively. 900 mode: When both values are lists: 901 - 'merge': Concatenate and dedupe (default) 902 - 'replace': Use addition's value, discard main's value 903 904 Returns: 905 Merged list of dictionaries. 906 """ 907 # Avoid modifying original collection 908 merged = deepcopy(main) 909 for addition in additions: 910 foundMain = findByKey(merged, key, addition.get(key)) 911 912 # Update existing dictionary 913 if foundMain: 914 for property in addition: 915 valueMain = foundMain.get(property) 916 valueAdd = addition.get(property) 917 # Update value if new 918 if not valueMain == valueAdd: 919 # Concat if both are lists 920 if all([isinstance(v, list) for v in [valueMain, valueAdd]]): 921 if mode == "replace": 922 valueNew = valueAdd 923 else: 924 valueNew = dedupe( 925 flatten([*valueMain, *valueAdd]), 926 caseSensitive=caseSensitive, 927 ) 928 # Otherwise overwrite 929 else: 930 valueNew = valueAdd 931 foundMain.update({property: valueNew}) 932 # Otherwise insert as last 933 else: 934 merged.append(addition) 935 return merged 936 937 938def flatMap(collection: list[dict], key: str) -> list: 939 """Map all `key` values into a single list. 940 941 Args: 942 collection: List of dictionaries. 943 key: Key to extract. 944 945 Returns: 946 Flattened list of all key values. 947 948 Example: 949 >>> data = [ 950 ... {"id": "a", "items": [1, 2, 3]}, 951 ... {"id": "b", "items": [4, 5]}, 952 ... ] 953 >>> flatMap(data, "items") 954 [1, 2, 3, 4, 5] 955 """ 956 assert isinstance(collection, list), "Collection must be a list" 957 assert isFlat(collection), "Collection must be flat list of dicts" 958 959 result = [] 960 for item in collection: 961 value = item.get(key, []) 962 # Handle strings (convert to list of characters) 963 if isinstance(value, str): 964 result.extend(list(value)) 965 # Handle iterables 966 elif isinstance(value, (list, tuple)): 967 result.extend(value) 968 # Handle single non-iterable values 969 else: 970 result.append(value) 971 return result 972 973 974# endregion
Strategy for filtering items in a list.
19def setLogLevel( 20 instance: "loguru.Logger", 21 level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG", 22): 23 """Set the log level and format for a loguru.Logger instance. 24 25 Args: 26 instance: The loguru.Logger instance to configure. 27 level: The log level to set. 28 """ 29 instance.remove() 30 instance.add( 31 sink=sys.stderr, 32 level=level.upper(), 33 format="<level>{level: ^8}</level> <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> <level>{message}</level>", 34 )
Set the log level and format for a loguru.Logger instance.
Arguments:
- instance: The loguru.Logger instance to configure.
- level: The log level to set.
39def isInRange(start: int, end: int, value: int): 40 """Returns True if value is in the inclusive range [start, end].""" 41 return start <= value <= end
Returns True if value is in the inclusive range [start, end].
49def truncateString(string: str, length=10, suffix="...") -> str: 50 """ 51 Truncate a string to a given length and append a suffix. 52 53 Example: 54 `(5)` `Hello there!` => `Hello...` 55 """ 56 return string[:length] + suffix
Truncate a string to a given length and append a suffix.
Example:
(5)Hello there!=>Hello...
64def coerceList(value, separate=False, strict=False) -> list: 65 """Coerce a value to a list. 66 67 Args: 68 value: The value to coerce. 69 separate: If True, split str to characters. 70 - (False) `"abc"` => `["abc"]` 71 - (True) `"abc"` => `["a", "b", "c"]` 72 strict: If True, force tuple to list. 73 - (False) `(a, b)` => `(a, b)` 74 - (True) `(a, b)` => `[(a, b)]` 75 76 Returns: 77 The coerced list. 78 79 Example: 80 - `1` or `[1]` => `[1]` 81 - `1, 2` => `[1, 2]` 82 """ 83 passingTypes = list if strict else (list, tuple, UserList) 84 # Check if not None, can be [] 85 if value is not None: 86 if isinstance(value, passingTypes): 87 return value 88 # Handle mapping views, e.g. dict.keys(), dict.values() 89 elif isinstance(value, MappingView): 90 return list(value) 91 elif isSequence(value): 92 return [value] 93 else: 94 return list(value) if separate else [value]
Coerce a value to a list.
Arguments:
- value: The value to coerce.
- separate: If True, split str to characters.
- (False)
"abc"=>["abc"] - (True)
"abc"=>["a", "b", "c"]
- (False)
- strict: If True, force tuple to list.
- (False)
(a, b)=>(a, b) - (True)
(a, b)=>[(a, b)]
- (False)
Returns:
The coerced list.
Example:
1or[1]=>[1]1, 2=>[1, 2]
97def coerceNumericList(value, separator=" ") -> list[int]: 98 """Coerce a value to a list of integers. 99 100 Args: 101 value: The value to coerce. 102 separator: Separator for splitting strings. 103 104 Returns: 105 List of integers. 106 107 Example: 108 - (int) `5` => `[5]` 109 - (str) `"5mm"` => `["5mm"]` 110 - (str) `"5 5"` => `[5, 5]` 111 """ 112 if isSequence(value): 113 return value 114 115 if isinstance(value, str): 116 if separator in value: 117 parts = value.split(separator) 118 try: 119 return [int(side) for side in parts] 120 except ValueError: 121 return parts 122 try: 123 return [int(value)] 124 except ValueError: 125 pass 126 127 return [value]
Coerce a value to a list of integers.
Arguments:
- value: The value to coerce.
- separator: Separator for splitting strings.
Returns:
List of integers.
Example:
- (int)
5=>[5] - (str)
"5mm"=>["5mm"] - (str)
"5 5"=>[5, 5]
130def coerceTuple(value) -> tuple: 131 """Coerce a value to a tuple. 132 133 Args: 134 value: The value to coerce. 135 136 Returns: 137 The coerced tuple. 138 139 Example: 140 - `1` or `(1,)` => `(1,)` 141 - `1, 2` => `(1, 2)` 142 """ 143 if isinstance(value, tuple): 144 return value 145 elif value is not None: 146 if isinstance(value, list): 147 return tuple(value) 148 if isinstance(value, str) and " " in value: 149 return tuple(value.split()) 150 return (value,)
Coerce a value to a tuple.
Arguments:
- value: The value to coerce.
Returns:
The coerced tuple.
Example:
1or(1,)=>(1,)1, 2=>(1, 2)
153def expand( 154 input, n=2, defaultValue=None, format: Literal["auto", "list", "tuple"] = "auto" 155) -> list | tuple: 156 """Expand to iterable with length of `n`. 157 158 Args: 159 input: The input value(s) to expand. 160 n: Desired length. 161 defaultValue: Fallback value for cycling. 162 format: Output type. 163 164 Returns: 165 Iterable of length n. 166 167 Example: 168 - `n=3`: `a` => `[a, a, a]` 169 - `n=4`: `[1, 2]` => `[1, 2, 1, 2]` 170 """ 171 172 def _getValueAtIndex(i: int): 173 try: 174 return input[i] 175 except IndexError: 176 return defaultValue if defaultValue is not None else input[i % len(input)] 177 178 input = coerceList(input) 179 if format == "auto": 180 outputFormat = tuple if isinstance(input, tuple) else list 181 else: 182 outputFormat = tuple if format == "tuple" else list 183 return outputFormat([_getValueAtIndex(i) for i in range(n)])
Expand to iterable with length of n.
Arguments:
- input: The input value(s) to expand.
- n: Desired length.
- defaultValue: Fallback value for cycling.
- format: Output type.
Returns:
Iterable of length n.
Example:
n=3:a=>[a, a, a]n=4:[1, 2]=>[1, 2, 1, 2]
186def reverseList(input: list[T]) -> list[T]: 187 """Inverse values in iterable. 188 189 Returns: 190 The reversed list. 191 """ 192 return input[::-1]
Inverse values in iterable.
Returns:
The reversed list.
195def previewList(input: list, length=3) -> str: 196 """ 197 Get a preview of the list with a specified number of items and a count of remaining items. 198 199 Example: 200 (2) `[A, B, C, D]` => `[A, B] and 2 more items` 201 """ 202 remainder = len(input) - length 203 noun = "items" if remainder > 1 else "item" 204 205 output = f"{input[:length]}" 206 207 if remainder > 0: 208 output += f" and {remainder} more {noun}" 209 210 return output
Get a preview of the list with a specified number of items and a count of remaining items.
Example:
(2)
[A, B, C, D]=>[A, B] and 2 more items
213def sampleList(input: list[T], size: int, shuffle=True) -> list[T]: 214 """Get random sample of specified size from list. 215 216 Args: 217 input: List to sample from. 218 size: Number of items to sample. 219 shuffle: If True, shuffle the list before sampling. 220 221 Returns: 222 Random sample of specified size. 223 """ 224 if shuffle: 225 return random.sample(input, min(size, len(input))) 226 return input[:size]
Get random sample of specified size from list.
Arguments:
- input: List to sample from.
- size: Number of items to sample.
- shuffle: If True, shuffle the list before sampling.
Returns:
Random sample of specified size.
229def inverseValues(values: list[Any]) -> list[Any]: 230 """Negate all values in a list. 231 232 Example: 233 - `[1, 2]` => `[-1, -2]` 234 - ["a", "b"]` => `["-a", "-b"]` 235 """ 236 237 def _negate(value): 238 if isinstance(value, (int, float)): 239 return -value 240 elif isinstance(value, str): 241 return f"-{value}" 242 else: 243 raise TypeError(f"Unsupported type for negation: {type(value).__name__}") 244 245 return [_negate(value) for value in coerceList(values)]
Negate all values in a list.
Example:
[1, 2]=>[-1, -2]- ["a", "b"]
=>["-a", "-b"]`
248def flatten(items: list): 249 """Flatten a list of lists or tuples. 250 251 Returns: 252 Flattened list or original input if not all items are lists/tuples. 253 """ 254 areItemsLists = all([isinstance(item, list) for item in items]) 255 areItemsTuples = all([isinstance(item, tuple) for item in items]) 256 257 if areItemsLists or areItemsTuples: 258 start = [] if areItemsLists else () 259 return list(sum(items, start)) 260 # Otherwise pass through unchanged 261 else: 262 isIterable = isinstance(items, (list, tuple)) 263 if not isIterable: 264 loguru.logger.debug("Unable to flatten: {}", items) 265 return items
Flatten a list of lists or tuples.
Returns:
Flattened list or original input if not all items are lists/tuples.
268def isFlat(items: list) -> bool: 269 """Returns True if all items in the list are not lists or tuples. 270 271 Example: 272 `[1, 2, 3]` => `True` 273 `[[1, 2], 3]` => `False` 274 """ 275 return all([not isinstance(item, (list, tuple)) for item in items])
Returns True if all items in the list are not lists or tuples.
Example:
[1, 2, 3]=>True[[1, 2], 3]=>False
278def flattenTuples(input): 279 """Flatten nested tuples and lists into a single list of tuples. 280 281 Example: 282 `[(1, 2), [(3, 4), (5, 6)]]` => `[(1, 2), (3, 4), (5, 6)]` 283 """ 284 output = [] 285 286 if isinstance(input, tuple): 287 input = [input] 288 289 for item in input: 290 if isinstance(item, list): 291 for subitem in item: 292 output.append(subitem) 293 else: 294 output.append(item) 295 return output
Flatten nested tuples and lists into a single list of tuples.
Example:
[(1, 2), [(3, 4), (5, 6)]]=>[(1, 2), (3, 4), (5, 6)]
298def intersect(lists: list[list], retainOrder=True) -> list: 299 """Return intersection of multiple lists. 300 301 Args: 302 lists: List of lists to intersect. 303 retainOrder: If True, order based on first list. 304 305 Returns: 306 List of intersected items. 307 308 Example: 309 `[a, b, c]` + `[b]` => `[b]` 310 """ 311 result = set.intersection(*map(set, lists)) 312 listA = lists[0] 313 314 return sorted(result, key=lambda x: listA.index(x)) if retainOrder else list(result)
Return intersection of multiple lists.
Arguments:
- lists: List of lists to intersect.
- retainOrder: If True, order based on first list.
Returns:
List of intersected items.
Example:
[a, b, c]+[b]=>[b]
317def zipWithMode( 318 groups: list[list], mode: Literal["drop", "fill"] = "fill" 319) -> list[list]: 320 """Zip lists with different modes. 321 322 Args: 323 groups: List of lists to zip. 324 mode: 'drop' to zip to shortest, 'fill' to zip to longest. 325 326 Returns: 327 Zipped list of lists. 328 """ 329 330 def _fillGroup(group): 331 isShorter = len(group) < longest 332 return [group[i % len(group)] for i in range(longest)] if isShorter else group 333 334 if mode == "fill": 335 groups = [group if group else [group] for group in groups] # None => [None] 336 longest = max([len(group) for group in groups]) 337 groups = [_fillGroup(group) for group in groups] 338 339 return list(zip(*groups))
Zip lists with different modes.
Arguments:
- groups: List of lists to zip.
- mode: 'drop' to zip to shortest, 'fill' to zip to longest.
Returns:
Zipped list of lists.
342def removeFalsy( 343 input: list, mode: Literal[None, False, "empty", "either"] = "either" 344) -> list: 345 """Remove falsy values from a list. 346 347 Args: 348 input: List to filter. 349 mode: 'None', 'False', 'empty', or 'either'. 350 351 Returns: 352 Filtered list. 353 """ 354 355 def _checkItem(item): 356 isNotNone = item is not None 357 isNotFalse = item is not False 358 isNotEmpty = isinstance(item, list) and len(item) 359 isNotEither = isNotNone and isNotFalse and isNotEmpty 360 if mode is None: 361 return isNotNone 362 elif mode is False: 363 return isNotFalse 364 else: 365 return isNotEither 366 367 return list(filter(_checkItem, input))
Remove falsy values from a list.
Arguments:
- input: List to filter.
- mode: 'None', 'False', 'empty', or 'either'.
Returns:
Filtered list.
370def removeNone(input: list) -> list: 371 """Remove `None` only from a list. 372 373 Returns: 374 List with None values removed. 375 """ 376 return removeFalsy(input=input, mode=None)
Remove None only from a list.
Returns:
List with None values removed.
379def removeFromList(input: list, remove: list) -> list: 380 """Remove specified items from a list. 381 382 Example: 383 `[a, b, c]` + `[b]` => `[a, c]` 384 385 Returns: 386 List with specified items removed. 387 """ 388 output = [] 389 remove = flatten(coerceList(remove)) 390 391 for element in input: 392 if element not in remove: 393 output.append(element) 394 395 return output
Remove specified items from a list.
Example:
[a, b, c]+[b]=>[a, c]
Returns:
List with specified items removed.
398def dedupe( 399 input: list[str], sortAlphabetically=False, caseSensitive=False, debug=False 400): 401 """Remove duplicates in list (= uniq). 402 403 Args: 404 input: List to deduplicate. 405 sortAlphabetically: If True, sort output. 406 caseSensitive: If False, dedupe case-insensitively. 407 debug: If True, log duplicates. 408 409 Returns: 410 Deduplicated list. 411 """ 412 input = [item for item in input if item] # Remove None 413 output = list() 414 uniq = set() 415 416 for item in input: 417 itemCase = ( 418 item if caseSensitive or isinstance(item, (int, float)) else item.casefold() 419 ) 420 if itemCase not in uniq: 421 uniq.add(itemCase) 422 output.append(item) 423 elif debug: 424 loguru.logger.trace("[Dedupe] {}", item) 425 426 if sortAlphabetically: 427 output = sorted(output) 428 429 return output
Remove duplicates in list (= uniq).
Arguments:
- input: List to deduplicate.
- sortAlphabetically: If True, sort output.
- caseSensitive: If False, dedupe case-insensitively.
- debug: If True, log duplicates.
Returns:
Deduplicated list.
432def findClosestValue(values: list, value: int, sort=True, discardLarger=False) -> int: 433 """Return closest value in a sorted list. 434 435 If two values are equally close, return the smallest value. 436 437 Args: 438 values: List of values. 439 value: Value to compare. 440 sort: If True, sort the list. 441 discardLarger: If True, discard values larger than input. 442 443 Returns: 444 Closest value or None if list is empty. 445 """ 446 from bisect import bisect_left 447 448 # Discard larger values 449 if discardLarger: 450 values = [v for v in values if v <= value] 451 452 if sort: 453 values = sorted(values) 454 455 pos = bisect_left(values, value) 456 if pos == 0: 457 return values[0] if len(values) else None 458 if pos == len(values): 459 return values[-1] 460 before = values[pos - 1] 461 after = values[pos] 462 463 if after - value < value - before: 464 return after 465 else: 466 return before
Return closest value in a sorted list.
If two values are equally close, return the smallest value.
Arguments:
- values: List of values.
- value: Value to compare.
- sort: If True, sort the list.
- discardLarger: If True, discard values larger than input.
Returns:
Closest value or None if list is empty.
469def findClosestIndex(values: list, value: str) -> int: 470 """Find index of closest matching string in a list. 471 472 Useful for finding `sort()` key. 473 474 Example: 475 `["B", "F"], "Foo"` => returns `1` 476 477 Returns: 478 Index of closest match or None. 479 """ 480 if not value: 481 return None 482 483 indexFound = None 484 485 while len(value) and not isinstance(indexFound, int): 486 try: 487 indexFound = values.index(value) 488 except ValueError: 489 pass 490 491 value = value[:-1] 492 493 return indexFound
Find index of closest matching string in a list.
Useful for finding sort() key.
Example:
["B", "F"], "Foo"=> returns1
Returns:
Index of closest match or None.
496def isSequence(value) -> bool: 497 """Check if value is a sequence (list or tuple) but not a string.""" 498 return isinstance(value, Sequence) and not isinstance(value, (str, bytes))
Check if value is a sequence (list or tuple) but not a string.
501def isListOfOne(value) -> bool: 502 """Returns True if the list contains exactly one item. 503 504 Example: 505 - `[1, 2]` => `false` 506 - `1` => `false` 507 - `[1]` => `true` 508 """ 509 return isSequence(value) and len(value) == 1
Returns True if the list contains exactly one item.
Example:
[1, 2]=>false1=>false[1]=>true
512def isFirst( 513 parent: list | int, 514 child: list | int, 515 mode: Literal["indices", "lists"] = "indices", 516): 517 """Returns True if child is the first element in parent. 518 519 Args: 520 parent: Iterable or count. 521 child: Item or index. 522 mode: 'indices' or 'lists'. 523 """ 524 childIndex = ( 525 child if isinstance(child, int) and mode == "indices" else parent.index(child) 526 ) 527 return childIndex == 0
Returns True if child is the first element in parent.
Arguments:
- parent: Iterable or count.
- child: Item or index.
- mode: 'indices' or 'lists'.
530def isLast( 531 parent: list | int, 532 child: list | int, 533 mode: Literal["indices", "lists"] = "indices", 534): 535 """Returns True if child is the last element in parent. 536 537 Args: 538 parent: Iterable or count. 539 child: Item or index. 540 mode: 'indices' or 'lists'. 541 """ 542 childIndex = ( 543 child if isinstance(child, int) and mode == "indices" else parent.index(child) 544 ) 545 parentCount = len(parent) if not isinstance(parent, int) else parent 546 return childIndex == parentCount - 1
Returns True if child is the last element in parent.
Arguments:
- parent: Iterable or count.
- child: Item or index.
- mode: 'indices' or 'lists'.
549def isNth(index: int, nth: int = 3): 550 """Returns True if index is every nth element. 551 552 Args: 553 index: Index to check. 554 nth: Nth interval. 555 556 Example: 557 `nth=3` => Every third 558 """ 559 return (index + 1) % nth == 0
Returns True if index is every nth element.
Arguments:
- index: Index to check.
- nth: Nth interval.
Example:
nth=3=> Every third
562def pickFirst(value): 563 """Get first value from iterable if iterable. 564 565 Returns: 566 First value or value itself if not iterable. 567 """ 568 if isSequence(value): 569 return value[0] 570 else: 571 return value
Get first value from iterable if iterable.
Returns:
First value or value itself if not iterable.
574def pickLast(value): 575 """Get last value from iterable if iterable. 576 577 Returns: 578 Last value or value itself if not iterable. 579 """ 580 if isSequence(value): 581 return value[-1] 582 else: 583 return value
Get last value from iterable if iterable.
Returns:
Last value or value itself if not iterable.
586def pickRandom(items: list) -> tuple: 587 """Pick 1 item at random and return remaining items. 588 589 Returns: 590 Tuple of (item, remainingItems). 591 """ 592 item = random.choice(items) 593 itemIndex = items.index(item) 594 items.pop(itemIndex) 595 596 return item, items
Pick 1 item at random and return remaining items.
Returns:
Tuple of (item, remainingItems).
599def pickExtremes(values: list): 600 """Get first and last from list. 601 602 Example: 603 `[1, 2, 3, 4]` => `(1, 4)` 604 605 Returns: 606 Tuple of (first, last) or values itself if length <= 1. 607 """ 608 if len(values) > 1: 609 return values[0], values[-1] 610 else: 611 return values
Get first and last from list.
Example:
[1, 2, 3, 4]=>(1, 4)
Returns:
Tuple of (first, last) or values itself if length <= 1.
614def pushToEnd(items: list[T], index=0): 615 """Move item at index to the end and return modified list. 616 617 Example: 618 `ABCD` `index=0` => `BCDA` 619 620 Returns: 621 Modified list. 622 """ 623 result = items.copy() # Avoid mutating original 624 result.append(result.pop(index)) 625 return result
Move item at index to the end and return modified list.
Example:
ABCDindex=0=>BCDA
Returns:
Modified list.
643def halven(input: T) -> list[T]: 644 """Split input into two halves. Works for lists and strings. 645 646 - `even`: two equal halves 647 - `odd`: larger/smaller half 648 649 Returns: 650 List of two halves (even or odd sized). 651 """ 652 return list(_splitToGroups(input, 2))
Split input into two halves. Works for lists and strings.
even: two equal halvesodd: larger/smaller half
Returns:
List of two halves (even or odd sized).
655def splitEvenly(input: T, groups: int = 3) -> list[T]: 656 """Split input into evenly sized groups. Works for lists and strings. 657 658 Args: 659 input: List or string to split. 660 groups: Number of groups. 661 662 Example: 663 `groups=2`, `ABCD` => `AB CD` 664 665 Returns: 666 List of groups. 667 """ 668 return list(_splitToGroups(input, groups))
Split input into evenly sized groups. Works for lists and strings.
Arguments:
- input: List or string to split.
- groups: Number of groups.
Example:
groups=2,ABCD=>AB CD
Returns:
List of groups.
671def toChunks(items: list[T], groups=2) -> list[list[T]]: 672 """Combine groups in alternating manner. 673 674 Example: 675 `groups=2`, `ABCD` => `AC BD` 676 677 Returns: 678 List of chunked groups. 679 """ 680 return [items[start::groups] for start in range(groups)]
Combine groups in alternating manner.
Example:
groups=2,ABCD=>AC BD
Returns:
List of chunked groups.
683def shuffleAtRandomSegment(items: list[T]) -> list[T]: 684 """Shuffle list at a random segment to maintain order. 685 686 Returns: 687 List shuffled at a random index. 688 """ 689 randomItem = random.choice(items) 690 randomIndex = items.index(randomItem) 691 return flatten([items[randomIndex:], items[:randomIndex]])
Shuffle list at a random segment to maintain order.
Returns:
List shuffled at a random index.
694def shuffleAtInterval(items: list[T], n: int = 2) -> list[T]: 695 """ 696 Rearrange a list by grouping every nth item together, then flattening the result. 697 698 Example: 699 - For items `[A, B, C, D, E, F]` and n `3`: 700 - The list is split into 3 groups: `[A, D], [B, E], [C, F]` 701 - The result is: `[A, D, B, E, C, F]` 702 703 Returns: 704 A new list with items reordered by interval. 705 """ 706 return sum(toChunks(items, n), [])
Rearrange a list by grouping every nth item together, then flattening the result.
Example:
- For items
[A, B, C, D, E, F]and n3: - The list is split into 3 groups:
[A, D], [B, E], [C, F] - The result is:
[A, D, B, E, C, F]
Returns:
A new list with items reordered by interval.
709def spaceEvenly(items: list, size=2): 710 """Get n items from list, evenly spaced. 711 712 Args: 713 items: List of items. 714 size: Number of items to get. 715 716 Example: 717 - 1 => get midpoint 718 - 2 => get first and last 719 720 Returns: 721 List of evenly spaced items. 722 """ 723 724 def _getSpacedIndices(start, stop, length): 725 return [round(start + x * (stop - start) / (length - 1)) for x in range(length)] 726 727 itemCount = len(items) 728 if size == 1: 729 # Return midpoint 730 indices = [len(items) >> 1] 731 else: 732 # Never return size larger than itemCount 733 indices = _getSpacedIndices(0, itemCount - 1, min(itemCount, size)) 734 735 return [items[i] for i in indices]
Get n items from list, evenly spaced.
Arguments:
- items: List of items.
- size: Number of items to get.
Example:
- 1 => get midpoint
- 2 => get first and last
Returns:
List of evenly spaced items.
743def pick(object: dict, keys: list[str]) -> dict: 744 """Pick properties by `key`. 745 746 Args: 747 object: Dictionary to pick from. 748 keys: Keys to pick. 749 750 Returns: 751 Dictionary with only picked keys. 752 """ 753 return {k: v for k, v in object.items() if k in coerceList(keys)}
Pick properties by key.
Arguments:
- object: Dictionary to pick from.
- keys: Keys to pick.
Returns:
Dictionary with only picked keys.
756def omit(object: dict, keys: list[str]) -> dict: 757 """Remove properties by `key`. 758 759 Args: 760 object: Dictionary to omit from. 761 keys: Keys to omit. 762 763 Returns: 764 Dictionary with omitted keys. 765 """ 766 return {k: v for k, v in object.items() if k not in coerceList(keys)}
Remove properties by key.
Arguments:
- object: Dictionary to omit from.
- keys: Keys to omit.
Returns:
Dictionary with omitted keys.
769def omitBy(object: dict, omit=[0, None, []]) -> dict: 770 """Remove properties by `value`. 771 772 Remove falsy by default: `0`, `None`, `[]`. 773 774 Args: 775 object: Dictionary to filter. 776 omit: Values to omit. 777 778 Returns: 779 Dictionary with omitted values. 780 """ 781 return {k: v for k, v in object.items() if v not in coerceList(omit)}
Remove properties by value.
Remove falsy by default: 0, None, [].
Arguments:
- object: Dictionary to filter.
- omit: Values to omit.
Returns:
Dictionary with omitted values.
789def merge(dicts: list[dict]): 790 """Merge multiple instances of `dict`. 791 792 What happens to values: 793 - `str` gets overwritten by last occurrence 794 - `list` gets extended (`flatten` + `dedupe`) 795 796 Args: 797 dicts: List of dictionaries to merge. 798 799 Returns: 800 Merged dictionary. 801 """ 802 result = dict() 803 804 for d in dicts: 805 for [key, value] in d.items(): 806 hasKey = result.get(key) 807 if hasKey: 808 if isinstance(value, list): 809 result[key] = dedupe(flatten([result[key], value])) 810 else: 811 result[key] = value 812 else: 813 result[key] = value 814 815 return result
818def findByKey(items: list[dict], key: str, value, defaultValue=None) -> dict | None: 819 """Find item in list of dictionaries/objects by key. 820 821 Args: 822 items: List of dictionaries or objects. 823 key: Key to search for. 824 value: Value to match. 825 defaultValue: Value to return if not found. 826 827 Returns: 828 The found item or defaultValue. 829 """ 830 831 def getValue(item, key): 832 # Use built-in method for dictionaries 833 if isinstance(item, dict): 834 return item.get(key) 835 # Otherwise assume it’s an object 836 else: 837 return getattr(item, key) 838 839 return next((item for item in items if getValue(item, key) == value), defaultValue)
Find item in list of dictionaries/objects by key.
Arguments:
- items: List of dictionaries or objects.
- key: Key to search for.
- value: Value to match.
- defaultValue: Value to return if not found.
Returns:
The found item or defaultValue.
842def sortByKey(items: list[dict], key: str, defaultValue: int | str = None): 843 """Sort list of dictionaries by key. 844 845 Args: 846 items: List of dictionaries. 847 key: Key to sort by. 848 defaultValue: Value to use if key is missing. 849 850 Returns: 851 Sorted list or original if sorting fails. 852 """ 853 # May be [] 854 try: 855 return sorted(items, key=lambda item: item.get(key, defaultValue)) 856 except TypeError: 857 return items
Sort list of dictionaries by key.
Arguments:
- items: List of dictionaries.
- key: Key to sort by.
- defaultValue: Value to use if key is missing.
Returns:
Sorted list or original if sorting fails.
860def groupByKey(items: list[dict], key: str) -> list[list[dict]]: 861 """Group list of dictionaries by `key`. 862 863 Args: 864 items: List of dictionaries. 865 key: Key to group by. 866 867 Returns: 868 Grouped items as an iterator. 869 """ 870 return groupby(items, key=lambda item: item.get(key))
Group list of dictionaries by key.
Arguments:
- items: List of dictionaries.
- key: Key to group by.
Returns:
Grouped items as an iterator.
873def reject(items: list[dict], key: str, value): 874 """Remove items matching `key`:`value`. 875 876 Args: 877 items: List of dictionaries. 878 key: Key to check. 879 value: Value to reject. 880 881 Returns: 882 Filtered list. 883 """ 884 return [item for item in items if item.get(key) != value]
Remove items matching key:value.
Arguments:
- items: List of dictionaries.
- key: Key to check.
- value: Value to reject.
Returns:
Filtered list.
887def mergeCollections( 888 main: list[dict], 889 additions: list[dict], 890 key: str = "id", 891 caseSensitive=True, 892 mode: Literal["merge", "replace"] = "merge", 893) -> list[dict]: 894 """Merge two instances of `list[dict]` and dedupe by key. 895 896 Args: 897 main: Main list of dictionaries. 898 additions: List of dictionaries to merge in. 899 key: Key to match for merging. 900 caseSensitive: If False, dedupe case-insensitively. 901 mode: When both values are lists: 902 - 'merge': Concatenate and dedupe (default) 903 - 'replace': Use addition's value, discard main's value 904 905 Returns: 906 Merged list of dictionaries. 907 """ 908 # Avoid modifying original collection 909 merged = deepcopy(main) 910 for addition in additions: 911 foundMain = findByKey(merged, key, addition.get(key)) 912 913 # Update existing dictionary 914 if foundMain: 915 for property in addition: 916 valueMain = foundMain.get(property) 917 valueAdd = addition.get(property) 918 # Update value if new 919 if not valueMain == valueAdd: 920 # Concat if both are lists 921 if all([isinstance(v, list) for v in [valueMain, valueAdd]]): 922 if mode == "replace": 923 valueNew = valueAdd 924 else: 925 valueNew = dedupe( 926 flatten([*valueMain, *valueAdd]), 927 caseSensitive=caseSensitive, 928 ) 929 # Otherwise overwrite 930 else: 931 valueNew = valueAdd 932 foundMain.update({property: valueNew}) 933 # Otherwise insert as last 934 else: 935 merged.append(addition) 936 return merged
Merge two instances of list[dict] and dedupe by key.
Arguments:
- main: Main list of dictionaries.
- additions: List of dictionaries to merge in.
- key: Key to match for merging.
- caseSensitive: If False, dedupe case-insensitively.
- mode: When both values are lists:
- 'merge': Concatenate and dedupe (default)
- 'replace': Use addition's value, discard main's value
Returns:
Merged list of dictionaries.
939def flatMap(collection: list[dict], key: str) -> list: 940 """Map all `key` values into a single list. 941 942 Args: 943 collection: List of dictionaries. 944 key: Key to extract. 945 946 Returns: 947 Flattened list of all key values. 948 949 Example: 950 >>> data = [ 951 ... {"id": "a", "items": [1, 2, 3]}, 952 ... {"id": "b", "items": [4, 5]}, 953 ... ] 954 >>> flatMap(data, "items") 955 [1, 2, 3, 4, 5] 956 """ 957 assert isinstance(collection, list), "Collection must be a list" 958 assert isFlat(collection), "Collection must be flat list of dicts" 959 960 result = [] 961 for item in collection: 962 value = item.get(key, []) 963 # Handle strings (convert to list of characters) 964 if isinstance(value, str): 965 result.extend(list(value)) 966 # Handle iterables 967 elif isinstance(value, (list, tuple)): 968 result.extend(value) 969 # Handle single non-iterable values 970 else: 971 result.append(value) 972 return result
Map all key values into a single list.
Arguments:
- collection: List of dictionaries.
- key: Key to extract.
Returns:
Flattened list of all key values.
Example:
>>> data = [ ... {"id": "a", "items": [1, 2, 3]}, ... {"id": "b", "items": [4, 5]}, ... ] >>> flatMap(data, "items") [1, 2, 3, 4, 5]