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