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