pamda.pamda

   1from functools import reduce
   2from pamda.pamda_utils import pamda_utils
   3from pamda.pamda_fast import (
   4    __getForceDict__,
   5    __assocPath__,
   6    __groupByHashable__,
   7    __mergeDeep__,
   8    __pathOr__,
   9    __getKeyValues__,
  10)
  11from pamda.pamda_curry import curry_obj
  12from pamda import pamda_wrappers
  13from typing import Any
  14
  15
  16@pamda_wrappers.typed_curry_wrap
  17@pamda_wrappers.classmethod_wrap
  18class pamda(pamda_utils):
  19    def accumulate(self, fn, initial_accumulator, data: list):
  20        """
  21        Function:
  22
  23        - Returns an accumulated list of items by iterating a function starting with an accumulator over a list
  24
  25        Requires:
  26
  27        - `fn`:
  28            - Type: function | method
  29            - What: The function or method to reduce
  30            - Note: This function should have an arity of 2 (take two inputs)
  31            - Note: The first input should take the accumulator value
  32            - Note: The second input should take the data value
  33        -`initial_accumulator`:
  34            - Type: any
  35            - What: The initial item to pass into the function when starting the accumulation process
  36        - `data`:
  37            - Type: list
  38            - What: The list of items to iterate over
  39
  40        Example:
  41
  42        ```
  43        data=[1,2,3,4]
  44        pamda.accumulate(
  45            fn=pamda.add,
  46            initial_accumulator=0,
  47            data=data
  48        )
  49        #=> [1,3,6,10]
  50
  51        ```
  52        """
  53        fn = self.curry(fn)
  54        if fn.__arity__ != 2:
  55            raise Exception("`fn` must have an arity of 2 (take two inputs)")
  56        if not len(data) > 0:
  57            raise Exception(
  58                "`data` has a length of 0, however it must have a length of at least 1"
  59            )
  60        acc = initial_accumulator
  61        out = []
  62        for i in data:
  63            acc = fn(acc, i)
  64            out.append(acc)
  65        return out
  66
  67    def add(self, a: int | float, b: int | float):
  68        """
  69        Function:
  70
  71        - Adds two numbers
  72
  73        Requires:
  74
  75        - `a`:
  76            - Type: int | float
  77            - What: The first number to add
  78        - `b`:
  79            - Type: int | float
  80            - What: The second number to add
  81
  82        Example:
  83
  84        ```
  85        pamda.add(1, 2) #=> 3
  86        ```
  87        """
  88        return a + b
  89
  90    def adjust(self, index: int, fn, data: list):
  91        """
  92        Function:
  93
  94        - Adjusts an item in a list by applying a function to it
  95
  96        Requires:
  97
  98        - `index`:
  99            - Type: int
 100            - What: The 0 based index of the item in the list to adjust
 101            - Note: Indicies are accepted
 102            - Note: If the index is out of range, picks the (-)first / (+)last item
 103        - `fn`:
 104            - Type: function | method
 105            - What: The function to apply the index item to
 106            - Note: This is automatically curried
 107        - `data`:
 108            - Type: list
 109            - What: The list to adjust
 110
 111        Example:
 112
 113        ```
 114        data=[1,5,9]
 115        pamda.adjust(
 116            index=1,
 117            fn=pamda.inc,
 118            data=data
 119        ) #=> [1,6,9]
 120        ```
 121        """
 122        fn = self.curry(fn)
 123        index = self.clamp(-len(data), len(data) - 1, index)
 124        data[index] = fn(data[index])
 125        return data
 126
 127    def assocPath(self, path: list | str | int | tuple, value, data: dict):
 128        """
 129        Function:
 130
 131        - Ensures a path exists within a nested dictionary
 132        - Note: This updates the object in place, but also returns the object
 133
 134        Requires:
 135
 136        - `path`:
 137            - Type: list[str | int | tuple] | str | int | tuple
 138            - What: The path to check
 139            - Note: If a string is passed, assumes a single item path list with that string
 140        - `value`:
 141            - Type: any
 142            - What: The value to appropriate to the end of the path
 143        - `data`:
 144            - Type: dict
 145            - What: A dictionary in which to associate the given value to the given path
 146
 147        Example:
 148
 149        ```
 150        data={'a':{'b':1}}
 151        pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}}
 152        ```
 153        """
 154        if not isinstance(path, list):
 155            path = [path]
 156        reduce(__getForceDict__, path[:-1], data).__setitem__(path[-1], value)
 157        return data
 158
 159    def assocPathComplex(
 160        self, default, default_fn, path: list | int | float | tuple, data: dict
 161    ):
 162        """
 163        Function:
 164
 165        - Ensures a path exists within a nested dictionary
 166        - Note: This updates the object in place, but also returns the object
 167
 168        Requires:
 169
 170        - `default`:
 171            - Type: any
 172            - What: The default item to add to a path that does not yet exist
 173        - `default_fn`:
 174            - Type: function | method
 175            - What: A unary (single input) function that takes in the current path item (or default) and adjusts it
 176            - Example: `lambda x: x` # Returns the value in the dict or the default value if none was present
 177        - `path`:
 178            - Type: list[str | int | tuple] | str | int | tuple
 179            - What: The path to check
 180        - `data`:
 181            - Type: dict
 182            - What: A dictionary to check if the path exists
 183
 184        Example:
 185
 186        ```
 187        data={'a':{'b':1}}
 188        pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}}
 189        ```
 190        """
 191        if self.getArity(default_fn) != 1:
 192            raise Exception(
 193                "`assocPathComplex` `default_fn` must be an unary (single input) function."
 194            )
 195        if not isinstance(path, list):
 196            path = [path]
 197        path_object = reduce(__getForceDict__, path[:-1], data)
 198        path_object.__setitem__(
 199            path[-1], default_fn(path_object.get(path[-1], default))
 200        )
 201        return data
 202
 203    def asyncKill(self, fn: curry_obj):
 204        """
 205        Function:
 206
 207        - Kills an asynchronous function that is currently running
 208        - Returns:
 209            - `None` if the function has not yet finished running
 210            - The result of the function if it has finished running
 211
 212        Requires:
 213
 214        - `fn`:
 215            - Type: thunkified function | thunkified method
 216            - What: The function or method to run asychronously
 217            - Note: The supplied `fn` must already be asynchronously running
 218
 219        Notes:
 220
 221        - See also `asyncRun` and `asyncWait`
 222        - A thunkified function currently running asynchronously can call `asyncKill` on itself
 223        - If a function has already finished running, calling `asyncKill` on it will have no effect
 224        - `asyncKill` does not kill threads that are sleeping (EG: `time.sleep`), but will kill the thread once the sleep is finished
 225
 226        Example:
 227
 228        ```
 229        import time
 230        from pamda import pamda
 231
 232        @pamda.thunkify
 233        def test(name, wait):
 234            waited = 0
 235            while waited < wait:
 236                time.sleep(1)
 237                waited += 1
 238                print(f'{name} has waited {waited} seconds')
 239
 240        async_test = pamda.asyncRun(test('a',3))
 241        time.sleep(1)
 242        pamda.asyncKill(async_test)
 243        # Alternatively:
 244        # async_test.asyncKill()
 245        ```
 246        """
 247        return fn.asyncKill()
 248
 249    def asyncRun(self, fn: curry_obj):
 250        """
 251        Function:
 252
 253        - Runs the supplied function asychronously
 254
 255        Requires:
 256
 257        - `fn`:
 258            - Type: thunkified function | thunkified method
 259            - What: The function or method to run asychronously
 260            - Note: The supplied `fn` must have an arity of 0
 261
 262        Notes:
 263
 264        - To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling `asyncRun` on it
 265        - To get the results of an `asyncRun` call `asyncWait`
 266        - To kill an `asyncRun` mid process call `asyncKill`
 267        - A thunkified function with arity of 0 can call `asyncRun` on itself
 268
 269        Examples:
 270
 271        Input:
 272        ```
 273        import time
 274
 275        @pamda.thunkify
 276        def test(name, wait):
 277            print(f'{name} start')
 278            time.sleep(wait)
 279            print(f'{name} end')
 280
 281        async_test = pamda.asyncRun(test('a',2))
 282        sync_test = test('b',1)()
 283        ```
 284        Output:
 285        ```
 286        a start
 287        b start
 288        b end
 289        a end
 290        ```
 291
 292
 293        Input:
 294        ```
 295        import time
 296
 297        @pamda.thunkify
 298        def test(name, wait):
 299            time.sleep(wait)
 300            return f"{name}: {wait}"
 301
 302        async_test = pamda.asyncRun(test('a',2))
 303        print(async_test.asyncWait()) #=> a: 2
 304        ```
 305
 306
 307        Input:
 308        ```
 309        import time
 310
 311        @pamda.thunkify
 312        def test(name, wait):
 313            time.sleep(wait)
 314            return f"{name}: {wait}"
 315
 316        async_test = test('a',2).asyncRun()
 317        print(async_test.asyncWait()) #=> a: 2
 318        ```
 319        """
 320        return fn.asyncRun()
 321
 322    def asyncWait(self, fn: curry_obj):
 323        """
 324        Function:
 325
 326        - Waits for a supplied function (if needed) and returns the results
 327
 328        Requires:
 329
 330        - `fn`:
 331            - Type: function | method
 332            - What: The function or method for which to wait
 333            - Note: The supplied `fn` must have previously called `asyncRun`
 334
 335        Notes:
 336
 337        - A thunkified function that has called `asyncRun` can call `asyncWait` on itself
 338
 339        Examples:
 340
 341        ```
 342        import time
 343
 344        @pamda.thunkify
 345        def test(name, wait):
 346            time.sleep(wait)
 347            return f"{name}: {wait}"
 348
 349        async_test = pamda.asyncRun(test('a',2))
 350        print(pamda.asyncWait(async_test)) #=> a: 2
 351        ```
 352
 353
 354        ```
 355        import time
 356
 357        @pamda.thunkify
 358        def test(name, wait):
 359            time.sleep(wait)
 360            return f"{name}: {wait}"
 361
 362        async_test = pamda.asyncRun(test('a',2))
 363        print(async_test.asyncWait()) #=> a: 2
 364        ```
 365        """
 366        return fn.asyncWait()
 367
 368    def clamp(self, minimum: int | float, maximum: int | float, a: int | float):
 369        """
 370        Function:
 371
 372        - Forces data to be within minimum and maximum
 373
 374        Requires:
 375
 376        - `minimum`:
 377            - Type: int | float
 378            - What: The minimum number
 379        - `maximum`:
 380            - Type: int | float
 381            - What: The maximum number
 382        - `a`:
 383            - Type: int | float
 384            - What: The number to clamp
 385
 386        Example:
 387
 388        ```
 389        pamda.clamp(1, 3, 2) #=> 2
 390        pamda.clamp(1, 3, 5) #=> 3
 391        ```
 392        """
 393        return min(max(a, minimum), maximum)
 394
 395    def curry(self, fn):
 396        """
 397        Function:
 398
 399        - Curries a function such that inputs can be added interatively
 400
 401        Requires:
 402
 403        - `fn`:
 404            - Type: function | method
 405            - What: The function or method to curry
 406            - Note: Class methods auto apply self during curry
 407
 408        Notes:
 409
 410        - Once curried, the function | method becomes a curry_obj object
 411        - The initial function is only called once all inputs are passed
 412
 413
 414        Examples:
 415
 416        ```
 417        curriedZip=pamda.curry(pamda.zip)
 418        curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]]
 419
 420        # Curried functions can be thunkified at any time
 421        # See also thunkify
 422        zipThunk=curriedZip.thunkify()(['a','b'])([1,2])
 423        zipThunk() #=> [['a',1],['b',2]]
 424        ```
 425
 426        ```
 427        def myFunction(a,b,c):
 428            return [a,b,c]
 429
 430        curriedMyFn=pamda.curry(myFunction)
 431
 432        curriedMyFn(1,2,3) #=> [1,2,3]
 433        curriedMyFn(1)(2,3) #=> [1,2,3]
 434
 435        x=curriedMyFn(1)(2)
 436        x(3) #=> [1,2,3]
 437        x(4) #=> [1,2,4]
 438
 439
 440        ```
 441        """
 442        if fn.__dict__.get("__isCurried__"):
 443            return fn()
 444        return curry_obj(fn)
 445
 446    def curryTyped(self, fn):
 447        """
 448        Function:
 449
 450        - Curries a function such that inputs can be added interatively and function annotations are type checked at runtime
 451
 452        Requires:
 453
 454        - `fn`:
 455            - Type: function | method
 456            - What: The function or method to curry
 457            - Note: Class methods auto apply self during curry
 458
 459        Notes:
 460
 461        - Once curried, the function | method becomes a curry_obj object
 462        - The initial function is only called once all inputs are passed
 463
 464
 465        Examples:
 466
 467        ```
 468        @pamda.curryTyped
 469        def add(a:int,b:int):
 470            return a+b
 471
 472        add(1)(1) #=> 2
 473        add(1)(1.5) #=> Raises type exception
 474        ```
 475        """
 476        if fn.__dict__.get("__isCurried__"):
 477            return fn().typeEnforce()
 478        return curry_obj(fn).typeEnforce()
 479
 480    def dec(self, a: int | float):
 481        """
 482        Function:
 483
 484        - Decrements a number by one
 485
 486        Requires:
 487
 488        - `a`:
 489            - Type: int | float
 490            - What: The number to decrement
 491
 492        Example:
 493
 494        ```
 495        pamda.dec(42) #=> 41
 496        ```
 497        """
 498        if not isinstance(a, (int, float)):
 499            raise Exception("`a` must be an `int` or a `float`")
 500        return a - 1
 501
 502    def difference(self, a: list, b: list):
 503        """
 504        Function:
 505
 506        - Combines two lists into a list of no duplicate items present in the first list but not the second
 507
 508        Requires:
 509
 510        - `a`:
 511            - Type: list
 512            - What: List of items in which to look for a difference
 513        - `b`:
 514            - Type: list
 515            - What: List of items in which to compare when looking for the difference
 516
 517        Example:
 518
 519        ```
 520        a=['a','b']
 521        b=['b','c']
 522        pamda.difference(a=a, b=b) #=> ['a']
 523        pamda.difference(a=b, b=a) #=> ['c']
 524        ```
 525        """
 526        return list(set(a).difference(set(b)))
 527
 528    def dissocPath(self, path: list | str | int | tuple, data: dict):
 529        """
 530        Function:
 531
 532        - Removes the value at the end of a path within a nested dictionary
 533        - Note: This updates the object in place, but also returns the object
 534
 535        Requires:
 536
 537        - `path`:
 538            - Type: list of strs | str
 539            - What: The path to remove from the dictionary
 540            - Note: If a string is passed, assumes a single item path list with that string
 541        - `data`:
 542            - Type: dict
 543            - What: A dictionary with a path to be removed
 544
 545        Example:
 546
 547        ```
 548        data={'a':{'b':{'c':0,'d':1}}}
 549        pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}}
 550        ```
 551        """
 552        if not isinstance(path, list):
 553            path = [path]
 554        if not self.hasPath(path=path, data=data):
 555            raise Exception("Path does not exist")
 556        else:
 557            reduce(__getForceDict__, path[:-1], data).pop(path[-1])
 558        return data
 559
 560    def flatten(self, data: list):
 561        """
 562        Function:
 563
 564        - Flattens a list of lists of lists ... into a single list depth first
 565
 566        Requires:
 567
 568        - `data`:
 569            - Type: list of lists
 570            - What: The list of lists to reduce to a single list
 571        Example:
 572
 573        ```
 574        data=[['a','b'],[1,[2]]]
 575        pamda.flatten(data=data) #=> ['a','b',1,2]
 576        ```
 577        """
 578
 579        def iter_flatten(data):
 580            out = []
 581            for i in data:
 582                if isinstance(i, list):
 583                    out.extend(iter_flatten(i))
 584                else:
 585                    out.append(i)
 586            return out
 587
 588        return iter_flatten(data)
 589
 590    def flip(self, fn):
 591        """
 592        Function:
 593
 594        - Returns a new function equivalent to the supplied function except that the first two inputs are flipped
 595
 596        Requires:
 597
 598        - `fn`:
 599            - Type: function | method
 600            - What: The function or method to flip
 601            - Note: This function must have an arity of at least 2 (take two inputs)
 602            - Note: Only args are flipped, kwargs are passed as normal
 603
 604        Notes:
 605
 606        - Input functions are not flipped in place
 607        - The returned function is a flipped version of the input function
 608        - A curried function can be flipped in place by calling fn.flip()
 609        - A function can be flipped multiple times:
 610            - At each flip, the first and second inputs for the function as it is currently curried are switched
 611            - Flipping a function two times before adding an input will return the initial value
 612
 613        Examples:
 614
 615        ```
 616        def concat(a,b,c,d):
 617            return str(a)+str(b)+str(c)+str(d)
 618
 619        flip_concat=pamda.flip(concat)
 620
 621        concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
 622        flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
 623        ```
 624
 625        ```
 626        @pamda.curry
 627        def concat(a,b,c,d):
 628            return str(a)+str(b)+str(c)+str(d)
 629
 630        concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
 631
 632        concat.flip()
 633
 634        concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
 635        ```
 636
 637        ```
 638        @pamda.curry
 639        def concat(a,b,c,d):
 640            return str(a)+str(b)+str(c)+str(d)
 641
 642        a=pamda.flip(concat)('fi-')
 643        b=pamda.flip(a)('fo-')
 644        c=pamda.flip(b)('fum')
 645        c('fe-') #=> 'fe-fi-fo-fum'
 646        ```
 647
 648        ```
 649        def concat(a,b,c,d):
 650            return str(a)+str(b)+str(c)+str(d)
 651
 652        a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum')
 653        a('fe-') #=> 'fe-fi-fo-fum'
 654        ```
 655        """
 656        fn = self.curry(fn)
 657        return fn.flip()
 658
 659    def getArity(self, fn):
 660        """
 661        Function:
 662
 663        - Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried)
 664
 665        Requires:
 666
 667        - `fn`:
 668            - Type: function | method
 669            - What: The function or method to get the arity of
 670            - Note: Class methods remove one arity to account for self
 671
 672        Examples:
 673
 674        ```
 675        pamda.getArity(pamda.zip) #=> 2
 676        curriedZip=pamda.curry(pamda.zip)
 677        ABCuriedZip=curriedZip(['a','b'])
 678        pamda.getArity(ABCuriedZip) #=> 1
 679        ```
 680        """
 681        fn = self.curry(fn)
 682        return fn.__arity__
 683
 684    def groupBy(self, fn, data: list):
 685        """
 686        Function:
 687
 688        - Splits a list into a dictionary of sublists keyed by the return string of a provided function
 689
 690        Requires:
 691
 692        - `fn`:
 693            - Type: function | method
 694            - What: The function or method to group by
 695            - Note: Must return a string (or other hashable object)
 696            - Note: This function must be unary (take one input)
 697            - Note: This function is applied to each item in the list recursively
 698        - `data`:
 699            - Type: list
 700            - What: List of items to apply the function to and then group by the results
 701
 702        Examples:
 703
 704        ```
 705        def getGrade(item):
 706            score=item['score']
 707            if score>90:
 708                return 'A'
 709            elif score>80:
 710                return 'B'
 711            elif score>70:
 712                return 'C'
 713            elif score>60:
 714                return 'D'
 715            else:
 716                return 'F'
 717
 718        data=[
 719            {'name':'Connor', 'score':75},
 720            {'name':'Fred', 'score':79},
 721            {'name':'Joe', 'score':84},
 722        ]
 723        pamda.groupBy(getGrade,data)
 724        #=>{
 725        #=>    'B':[{'name':'Joe', 'score':84}]
 726        #=>    'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}]
 727        #=>}
 728        ```
 729        """
 730        curried_fn = self.curry(fn)
 731        if curried_fn.__arity__ != 1:
 732            raise Exception(
 733                "groupBy `fn` must only take one parameter as its input"
 734            )
 735        return __groupByHashable__(fn=fn, data=data)
 736
 737    def groupKeys(self, keys: list, data: list):
 738        """
 739        Function:
 740
 741        - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys
 742
 743        Requires:
 744
 745        - `keys`:
 746            - Type: list of strs
 747            - What: The keys to group by
 748        - `data`:
 749            - Type: list of dicts
 750            - What: List of dictionaries with which to match keys
 751
 752        Examples:
 753
 754        ```
 755        data=[
 756            {'color':'red', 'size':9, 'shape':'ball'},
 757            {'color':'red', 'size':10, 'shape':'ball'},
 758            {'color':'green', 'size':11, 'shape':'ball'},
 759            {'color':'green', 'size':12, 'shape':'square'}
 760        ]
 761        pamda.groupKeys(['color','shape'],data)
 762        #=> [
 763        #=>     [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}],
 764        #=>     [{'color': 'green', 'size': 11, 'shape': 'ball'}],
 765        #=>     [{'color': 'green', 'size': 12, 'shape': 'square'}]
 766        #=> ]
 767        ```
 768        """
 769
 770        def key_fn(item):
 771            return tuple([item[key] for key in keys])
 772
 773        return list(__groupByHashable__(key_fn, data).values())
 774
 775    def groupWith(self, fn, data: list):
 776        """
 777        Function:
 778
 779        - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function
 780
 781        Requires:
 782
 783        - `fn`:
 784            - Type: function | method
 785            - What: The function or method to groub with
 786            - Note: Must return a boolean value
 787            - Note: This function must have an arity of two (take two inputs)
 788            - Note: This function is applied to each item plus the next adjacent item in the list recursively
 789        - `data`:
 790            - Type: list
 791            - What: List of items to apply the function to and then group the results
 792
 793        Examples:
 794
 795        ```
 796        def areEqual(a,b):
 797            return a==b
 798
 799        data=[1,2,3,1,1,2,2,3,3,3]
 800        pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]]
 801        ```
 802        """
 803        curried_fn = self.curry(fn)
 804        if curried_fn.__arity__ != 2:
 805            raise Exception("groupWith `fn` must take exactly two parameters")
 806        previous = data[0]
 807        output = []
 808        sublist = [previous]
 809        for i in data[1:]:
 810            if fn(i, previous):
 811                sublist.append(i)
 812            else:
 813                output.append(sublist)
 814                sublist = [i]
 815            previous = i
 816        output.append(sublist)
 817        return output
 818
 819    def hasPath(self, path: list | str, data: dict):
 820        """
 821        Function:
 822
 823        - Checks if a path exists within a nested dictionary
 824
 825        Requires:
 826
 827        - `path`:
 828            - Type: list of strs | str
 829            - What: The path to check
 830            - Note: If a string is passed, assumes a single item path list with that string
 831        - `data`:
 832            - Type: dict
 833            - What: A dictionary to check if the path exists
 834
 835        Example:
 836
 837        ```
 838        data={'a':{'b':1}}
 839        pamda.hasPath(path=['a','b'], data=data) #=> True
 840        pamda.hasPath(path=['a','d'], data=data) #=> False
 841        ```
 842        """
 843        if isinstance(path, str):
 844            path = [path]
 845        try:
 846            reduce(lambda x, y: x[y], path, data)
 847            return True
 848        except (KeyError, IndexError, TypeError):
 849            return False
 850
 851    def hardRound(self, decimal_places: int, a: int | float):
 852        """
 853        Function:
 854
 855        - Rounds to a set number of decimal places regardless of floating point math in python
 856
 857        Requires:
 858
 859        - `decimal_places`:
 860            - Type: int
 861            - What: The number of decimal places to round to
 862            - Default: 0
 863            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
 864        - `a`:
 865            - Type: int | float
 866            - What: The number to round
 867
 868        Example:
 869
 870        ```
 871        a=12.345
 872        pamda.hardRound(1,a) #=> 12.3
 873        pamda.hardRound(-1,a) #=> 10
 874        ```
 875        """
 876        return int(a * (10**decimal_places) + 0.5) / (10**decimal_places)
 877
 878    def head(self, data: list | str):
 879        """
 880        Function:
 881
 882        - Picks the first item out of a list or string
 883
 884        Requires:
 885
 886        - `data`:
 887            - Type: list | str
 888            - What: A list or string
 889
 890        Example:
 891
 892        ```
 893        data=['fe','fi','fo','fum']
 894        pamda.first(
 895            data=data
 896        ) #=> fe
 897        ```
 898        """
 899        if not isinstance(data, (list, str)):
 900            raise Exception("`head` can only be called on a `str` or a `list`")
 901        if not len(data) > 0:
 902            raise Exception("Attempting to call `head` on an empty list or str")
 903        return data[0]
 904
 905    def inc(self, a: int | float):
 906        """
 907        Function:
 908
 909        - Increments a number by one
 910
 911        Requires:
 912
 913        - `a`:
 914            - Type: int | float
 915            - What: The number to increment
 916
 917        Example:
 918
 919        ```
 920        pamda.inc(42) #=> 43
 921        ```
 922        """
 923        if not isinstance(a, (int, float)):
 924            raise Exception("`a` must be an `int` or a `float`")
 925        return a + 1
 926
 927    def intersection(self, a: list, b: list):
 928        """
 929        Function:
 930
 931        - Combines two lists into a list of no duplicates composed of those elements common to both lists
 932
 933        Requires:
 934
 935        - `a`:
 936            - Type: list
 937            - What: List of items in which to look for an intersection
 938        - `b`:
 939            - Type: list
 940            - What: List of items in which to look for an intersection
 941
 942        Example:
 943
 944        ```
 945        a=['a','b']
 946        b=['b','c']
 947        pamda.intersection(a=a, b=b) #=> ['b']
 948        ```
 949        """
 950        return list(set(a).intersection(set(b)))
 951
 952    def map(self, fn, data: list | dict):
 953        """
 954        Function:
 955
 956        - Maps a function over a list or a dictionary
 957
 958        Requires:
 959
 960        - `fn`:
 961            - Type: function | method
 962            - What: The function or method to map over the list or dictionary
 963            - Note: This function should have an arity of 1
 964        - `data`:
 965            - Type: list | dict
 966            - What: The list or dict of items to map the function over
 967
 968        Examples:
 969
 970        ```
 971        data=[1,2,3]
 972        pamda.map(
 973            fn=pamda.inc,
 974            data=data
 975        )
 976        #=> [2,3,4]
 977        ```
 978
 979        ```
 980        data={'a':1,'b':2,'c':3}
 981        pamda.map(
 982            fn=pamda.inc,
 983            data=data
 984        )
 985        #=> {'a':2,'b':3,'c':4}
 986        ```
 987
 988        """
 989        # TODO: Check for efficiency gains
 990        fn = self.curry(fn)
 991        if fn.__arity__ != 1:
 992            raise Exception("`map` `fn` must be unary (take one input)")
 993        if not len(data) > 0:
 994            raise Exception(
 995                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
 996            )
 997        if isinstance(data, dict):
 998            return {key: fn(value) for key, value in data.items()}
 999        else:
1000            return [fn(i) for i in data]
1001
1002    def mean(self, data: list):
1003        """
1004        Function:
1005
1006        - Calculates the mean of a given list
1007
1008        Requires:
1009
1010        - `data`:
1011            - Type: list of (floats | ints)
1012            - What: The list with wich to calculate the mean
1013            - Note: If the length of this list is 0, returns None
1014
1015        Example:
1016
1017        ```
1018        data=[1,2,3]
1019        pamda.mean(data=data)
1020        #=> 2
1021        ```
1022
1023        ```
1024        data=[]
1025        pamda.mean(data=data)
1026        #=> None
1027        ```
1028        """
1029        if len(data) == 0:
1030            return None
1031        return sum(data) / len(data)
1032
1033    def median(self, data: list):
1034        """
1035        Function:
1036
1037        - Calculates the median of a given list
1038        - If the length of the list is even, calculates the mean of the two central values
1039
1040        Requires:
1041
1042        - `data`:
1043            - Type: list of (floats | ints)
1044            - What: The list with wich to calculate the mean
1045            - Note: If the length of this list is 0, returns None
1046
1047        Examples:
1048
1049        ```
1050        data=[7,2,8,9]
1051        pamda.median(data=data)
1052        #=> 7.5
1053        ```
1054
1055        ```
1056        data=[7,8,9]
1057        pamda.median(data=data)
1058        #=> 8
1059        ```
1060
1061        ```
1062        data=[]
1063        pamda.median(data=data)
1064        #=> None
1065        ```
1066        """
1067        if not isinstance(data, (list)):
1068            raise Exception("`median` `data` must be a list")
1069        length = len(data)
1070        if length == 0:
1071            return None
1072        data = sorted(data)
1073        if length % 2 == 0:
1074            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1075        return data[int(length / 2)]
1076
1077    def mergeDeep(self, update_data, data):
1078        """
1079        Function:
1080
1081        - Recursively merges two nested dictionaries keeping all keys at each layer
1082        - Values from `update_data` are used when keys are present in both dictionaries
1083
1084        Requires:
1085
1086        - `update_data`:
1087            - Type: any
1088            - What: The new data that will take precedence during merging
1089        - `data`:
1090            - Type: any
1091            - What: The original data that will be merged into
1092
1093        Example:
1094
1095        ```
1096        data={'a':{'b':{'c':'d'},'e':'f'}}
1097        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1098        pamda.mergeDeep(
1099            update_data=update_data,
1100            data=data
1101        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1102        ```
1103        """
1104        return __mergeDeep__(update_data, data)
1105
1106    def nest(self, path_keys: list, value_key: str, data: list):
1107        """
1108        Function:
1109
1110        - Nests a list of dictionaries into a nested dictionary
1111        - Similar items are appended to a list in the end of the nested dictionary
1112
1113        Requires:
1114
1115        - `path_keys`:
1116            - Type: list of strs
1117            - What: The variables to pull from each item in data
1118            - Note: Used to build out the nested dicitonary
1119            - Note: Order matters as the nesting occurs in order of variable
1120        - `value_key`:
1121            - Type: str
1122            - What: The variable to add to the list at the end of the nested dictionary path
1123        - `data`:
1124            - Type: list of dicts
1125            - What: A list of dictionaries to use for nesting purposes
1126
1127        Example:
1128
1129        ```
1130        data=[
1131            {'x_1':'a','x_2':'b', 'output':'c'},
1132            {'x_1':'a','x_2':'b', 'output':'d'},
1133            {'x_1':'a','x_2':'e', 'output':'f'}
1134        ]
1135        pamda.nest(
1136            path_keys=['x_1','x_2'],
1137            value_key='output',
1138            data=data
1139        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1140        ```
1141        """
1142        if not isinstance(data, list):
1143            raise Exception("Attempting to `nest` an object that is not a list")
1144        if len(data) == 0:
1145            raise Exception("Attempting to `nest` from an empty list")
1146        nested_output = {}
1147        for item in self.groupKeys(keys=path_keys, data=data):
1148            nested_output = __assocPath__(
1149                path=__getKeyValues__(path_keys, item[0]),
1150                value=[i.get(value_key) for i in item],
1151                data=nested_output,
1152            )
1153        return nested_output
1154
1155    def nestItem(self, path_keys: list, data: list):
1156        """
1157        Function:
1158
1159        - Nests a list of dictionaries into a nested dictionary
1160        - Similar items are appended to a list in the end of the nested dictionary
1161        - Similar to `nest`, except no values are plucked for the aggregated list
1162
1163        Requires:
1164
1165        - `path_keys`:
1166            - Type: list of strs
1167            - What: The variables to pull from each item in data
1168            - Note: Used to build out the nested dicitonary
1169            - Note: Order matters as the nesting occurs in order of variable
1170        - `data`:
1171            - Type: list of dicts
1172            - What: A list of dictionaries to use for nesting purposes
1173
1174        Example:
1175
1176        ```
1177        data=[
1178            {'x_1':'a','x_2':'b'},
1179            {'x_1':'a','x_2':'b'},
1180            {'x_1':'a','x_2':'e'}
1181        ]
1182        pamda.nestItem
1183            path_keys=['x_1','x_2'],
1184            data=data
1185        )
1186        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1187
1188        ```
1189        """
1190        if not isinstance(data, list):
1191            raise Exception("Attempting to `nest` an object that is not a list")
1192        if len(data) == 0:
1193            raise Exception("Attempting to `nest` from an empty list")
1194        nested_output = {}
1195        for item in self.groupKeys(keys=path_keys, data=data):
1196            nested_output = __assocPath__(
1197                path=__getKeyValues__(path_keys, item[0]),
1198                value=item,
1199                data=nested_output,
1200            )
1201        return nested_output
1202
1203    def path(self, path: list | str, data: dict):
1204        """
1205        Function:
1206
1207        - Returns the value of a path within a nested dictionary or None if the path does not exist
1208
1209        Requires:
1210
1211        - `path`:
1212            - Type: list of strs | str
1213            - What: The path to pull given the data
1214            - Note: If a string is passed, assumes a single item path list with that string
1215        - `data`:
1216            - Type: dict
1217            - What: A dictionary to get the path from
1218
1219        Example:
1220
1221        ```
1222        data={'a':{'b':1}}
1223        pamda.path(path=['a','b'], data=data) #=> 1
1224        ```
1225        """
1226        if isinstance(path, str):
1227            path = [path]
1228        return __pathOr__(None, path, data)
1229
1230    def pathOr(self, default, path: list | str, data: dict):
1231        """
1232        Function:
1233
1234        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1235
1236        Requires:
1237
1238        - `default`:
1239            - Type: any
1240            - What: The object to return if the path does not exist
1241        - `path`:
1242            - Type: list of strs | str
1243            - What: The path to pull given the data
1244            - Note: If a string is passed, assumes a single item path list with that string
1245        - `data`:
1246            - Type: dict
1247            - What: A dictionary to get the path from
1248
1249        Example:
1250
1251        ```
1252        data={'a':{'b':1}}
1253        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1254        ```
1255        """
1256        if isinstance(path, str):
1257            path = [path]
1258        try:
1259            return reduce(lambda x, y: x[y], path, data)
1260        except (KeyError, IndexError, TypeError):
1261            return default
1262
1263    def pipe(self, fns: list, args: tuple, kwargs: dict):
1264        """
1265        Function:
1266
1267        - Pipes data through n functions in order (left to right composition) and returns the output
1268
1269        Requires:
1270
1271        - `fns`:
1272            - Type: list of (functions | methods)
1273            - What: The list of functions and methods to pipe the data through
1274            - Notes: The first function in the list can be any arity (accepting any number of inputs)
1275            - Notes: Any further function in the list can only be unary (single input)
1276            - Notes: A function can be curried, but is not required to be
1277            - Notes: You may opt to curry functions and add inputs to make them unary
1278        - `args`:
1279            - Type: tuple
1280            - What: a tuple of positional arguments to pass to the first function in `fns`
1281        - `kwargs`:
1282            - Type: dict
1283            - What: a dictionary of keyword arguments to pass to the first function in `fns`
1284
1285        Examples:
1286
1287        ```
1288        data=['abc','def']
1289        pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
1290        pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
1291        ```
1292
1293        ```
1294        data={'a':{'b':'c'}}
1295        curriedPath=pamda.curry(pamda.path)
1296        pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1297        ```
1298        """
1299        if len(fns) == 0:
1300            raise Exception("`fns` must be a list with at least one function")
1301        if self.getArity(fns[0]) == 0:
1302            raise Exception(
1303                "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0."
1304            )
1305        if not all([(self.getArity(fn) == 1) for fn in fns[1:]]):
1306            raise Exception(
1307                "Only the first function in `fns` can have n arity (accept n args). All other functions must have an arity of one (accepting one argument)."
1308            )
1309        out = fns[0](*args, **kwargs)
1310        for fn in fns[1:]:
1311            out = fn(out)
1312        return out
1313
1314    def pivot(self, data: list[dict] | dict[Any, list]):
1315        """
1316        Function:
1317
1318        - Pivots a list of dictionaries into a dictionary of lists
1319        - Pivots a dictionary of lists into a list of dictionaries
1320
1321        Requires:
1322
1323        - `data`:
1324            - Type: list of dicts | dict of lists
1325            - What: The data to pivot
1326            - Note: If a list of dictionaries is passed, all dictionaries must have the same keys
1327            - Note: If a dictionary of lists is passed, all lists must have the same length
1328
1329        Example:
1330
1331        ```
1332        data=[
1333            {'a':1,'b':2},
1334            {'a':3,'b':4}
1335        ]
1336        pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
1337
1338        data={'a':[1,3],'b':[2,4]}
1339        pamda.pivot(data=data)
1340        #=> [
1341        #=>     {'a':1,'b':2},
1342        #=>     {'a':3,'b':4}
1343        #=> ]
1344        ```
1345        """
1346        if isinstance(data, list):
1347            return {
1348                key: [record[key] for record in data] for key in data[0].keys()
1349            }
1350        else:
1351            return [
1352                {key: data[key][i] for key in data.keys()}
1353                for i in range(len(data[list(data.keys())[0]]))
1354            ]
1355
1356    def pluck(self, path: list | str, data: list):
1357        """
1358        Function:
1359
1360        - Returns the values of a path within a list of nested dictionaries
1361
1362        Requires:
1363
1364        - `path`:
1365            - Type: list of strs
1366            - What: The path to pull given the data
1367            - Note: If a string is passed, assumes a single item path list with that string
1368        - `data`:
1369            - Type: list of dicts
1370            - What: A list of dictionaries to get the path from
1371
1372        Example:
1373
1374        ```
1375        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1376        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1377        ```
1378        """
1379        if len(data) == 0:
1380            raise Exception("Attempting to pluck from an empty list")
1381        if isinstance(path, str):
1382            path = [path]
1383        return [__pathOr__(default=None, path=path, data=i) for i in data]
1384
1385    def pluckIf(self, fn, path: list | str, data: list):
1386        """
1387        Function:
1388
1389        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1390
1391        Requires:
1392
1393        - `fn`:
1394            - Type: function
1395            - What: A function to take in each item in data and return a boolean
1396            - Note: Only items that return true are plucked
1397            - Note: Should be a unary function (take one input)
1398        - `path`:
1399            - Type: list of strs
1400            - What: The path to pull given the data
1401            - Note: If a string is passed, assumes a single item path list with that string
1402        - `data`:
1403            - Type: list of dicts
1404            - What: A list of dictionary to get the path from
1405
1406        Example:
1407
1408        ```
1409
1410        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1411        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1412        ```
1413        """
1414        if len(data) == 0:
1415            raise Exception("Attempting to pluck from an empty list")
1416        curried_fn = self.curry(fn)
1417        if curried_fn.__arity__ != 1:
1418            raise Exception(
1419                "`pluckIf` `fn` must have an arity of 1 (take one input)"
1420            )
1421        if isinstance(path, str):
1422            path = [path]
1423        return [
1424            __pathOr__(default=None, path=path, data=i) for i in data if fn(i)
1425        ]
1426
1427    def project(self, keys: list[str], data: list[dict]):
1428        """
1429        Function:
1430
1431        - Returns a list of dictionaries with only the keys provided
1432        - Analogous to SQL's `SELECT` statement
1433
1434        Requires:
1435
1436        - `keys`:
1437            - Type: list of strs
1438            - What: The keys to select from each dictionary in the data list
1439        - `data`:
1440            - Type: list of dicts
1441            - What: The list of dictionaries to select from
1442
1443        Example:
1444
1445        ```
1446        data=[
1447            {'a':1,'b':2,'c':3},
1448            {'a':4,'b':5,'c':6}
1449        ]
1450        pamda.project(keys=['a','c'], data=data)
1451        #=> [
1452        #=>     {'a':1,'c':3},
1453        #=>     {'a':4,'c':6}
1454        #=> ]
1455        ```
1456        """
1457        return [{key: record[key] for key in keys} for record in data]
1458
1459    def props(self, keys: list[str], data: dict):
1460        """
1461        Function:
1462
1463        - Returns the values of a list of keys within a dictionary
1464
1465        Requires:
1466
1467        - `keys`:
1468            - Type: list of strs
1469            - What: The keys to pull given the data
1470        - `data`:
1471            - Type: dict
1472            - What: A dictionary to get the keys from
1473
1474        Example:
1475        ```
1476        data={'a':1,'b':2,'c':3}
1477        pamda.props(keys=['a','c'], data=data)
1478        #=> [1,3]
1479        ```
1480        """
1481        return [data[key] for key in keys]
1482
1483    def reduce(self, fn, initial_accumulator, data: list):
1484        """
1485        Function:
1486
1487        - Returns a single item by iterating a function starting with an accumulator over a list
1488
1489        Requires:
1490
1491        - `fn`:
1492            - Type: function | method
1493            - What: The function or method to reduce
1494            - Note: This function should have an arity of 2 (take two inputs)
1495            - Note: The first input should take the accumulator value
1496            - Note: The second input should take the data value
1497        -`initial_accumulator`:
1498            - Type: any
1499            - What: The initial item to pass into the function when starting the accumulation process
1500        - `data`:
1501            - Type: list
1502            - What: The list of items to iterate over
1503
1504        Example:
1505
1506        ```
1507        data=[1,2,3,4]
1508        pamda.reduce(
1509            fn=pamda.add,
1510            initial_accumulator=0,
1511            data=data
1512        )
1513        #=> 10
1514
1515        ```
1516        """
1517        fn = self.curry(fn)
1518        if fn.__arity__ != 2:
1519            raise Exception(
1520                "`reduce` `fn` must have an arity of 2 (take two inputs)"
1521            )
1522        if not isinstance(data, (list)):
1523            raise Exception("`reduce` `data` must be a list")
1524        if not len(data) > 0:
1525            raise Exception(
1526                "`reduce` `data` has a length of 0, however it must have a length of at least 1"
1527            )
1528        acc = initial_accumulator
1529        for i in data:
1530            acc = fn(acc, i)
1531        return acc
1532
1533    def safeDivide(self, denominator: int | float, a: int | float):
1534        """
1535        Function:
1536
1537        - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
1538
1539        Requires:
1540
1541        - `denominator`:
1542            - Type: int | float
1543            - What: The denominator
1544
1545        - `a`:
1546            - Type: int | float
1547            - What: The numerator
1548
1549        Example:
1550
1551        ```
1552        pamda.safeDivide(2,10) #=> 5
1553        pamda.safeDivide(0,10) #=> 10
1554        ```
1555        """
1556        return a / denominator if denominator != 0 else a
1557
1558    def safeDivideDefault(
1559        self,
1560        default_denominator: int | float,
1561        denominator: int | float,
1562        a: int | float,
1563    ):
1564        """
1565        Function:
1566
1567        - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
1568
1569        Requires:
1570
1571        - `default_denominator`:
1572            - Type: int | float
1573            - What: A non zero denominator to use if denominator is zero
1574            - Default: 1
1575        - `denominator`:
1576            - Type: int | float
1577            - What: The denominator
1578        - `a`:
1579            - Type: int | float
1580            - What: The numerator
1581
1582        Example:
1583
1584        ```
1585        pamda.safeDivideDefault(2,5,10) #=> 2
1586        pamda.safeDivideDefault(2,0,10) #=> 5
1587        ```
1588        """
1589        if default_denominator == 0:
1590            raise Exception(
1591                "`safeDivideDefault` `default_denominator` can not be 0"
1592            )
1593        return a / denominator if denominator != 0 else a / default_denominator
1594
1595    def symmetricDifference(self, a: list, b: list):
1596        """
1597        Function:
1598
1599        - Combines two lists into a list of no duplicates items present in one list but not the other
1600
1601        Requires:
1602
1603        - `a`:
1604            - Type: list
1605            - What: List of items in which to look for a difference
1606        - `b`:
1607            - Type: list
1608            - What: List of items in which to look for a difference
1609
1610        Example:
1611
1612        ```
1613        a=['a','b']
1614        b=['b','c']
1615        pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1616        ```
1617        """
1618        return list(set(a).difference(set(b))) + list(set(b).difference(set(a)))
1619
1620    def tail(self, data: list | str):
1621        """
1622        Function:
1623
1624        - Picks the last item out of a list or string
1625
1626        Requires:
1627
1628        - `data`:
1629            - Type: list | str
1630            - What: A list or string
1631
1632        Example:
1633
1634        ```
1635        data=['fe','fi','fo','fum']
1636        pamda.tail(
1637            data=data
1638        ) #=> fum
1639        ```
1640        """
1641        if not len(data) > 0:
1642            raise Exception("Attempting to call `tail` on an empty list or str")
1643        return data[-1]
1644
1645    def thunkify(self, fn):
1646        """
1647        Function:
1648
1649        - Creates a curried thunk out of a function
1650        - Evaluation of the thunk lazy and is delayed until called
1651
1652        Requires:
1653
1654        - `fn`:
1655            - Type: function | method
1656            - What: The function or method to thunkify
1657            - Note: Thunkified functions are automatically curried
1658            - Note: Class methods auto apply self during thunkify
1659
1660        Notes:
1661
1662        - Input functions are not thunkified in place
1663        - The returned function is a thunkified version of the input function
1664        - A curried function can be thunkified in place by calling fn.thunkify()
1665
1666        Examples:
1667
1668        ```
1669        def add(a,b):
1670            return a+b
1671
1672        addThunk=pamda.thunkify(add)
1673
1674        add(1,2) #=> 3
1675        addThunk(1,2)
1676        addThunk(1,2)() #=> 3
1677
1678        x=addThunk(1,2)
1679        x() #=> 3
1680        ```
1681
1682        ```
1683        @pamda.curry
1684        def add(a,b):
1685            return a+b
1686
1687        add(1,2) #=> 3
1688
1689        add.thunkify()
1690
1691        add(1,2)
1692        add(1,2)() #=> 3
1693        ```
1694        """
1695        fn = self.curry(fn)
1696        return fn.thunkify()
1697
1698    def unnest(self, data: list):
1699        """
1700        Function:
1701
1702        - Removes one level of depth for all items in a list
1703
1704        Requires:
1705
1706        - `data`:
1707            - Type: list
1708            - What: A list of items to unnest by one level
1709
1710        Examples:
1711
1712        ```
1713        data=['fe','fi',['fo',['fum']]]
1714        pamda.unnest(
1715            data=data
1716        ) #=> ['fe','fi','fo',['fum']]
1717        ```
1718        """
1719        if not len(data) > 0:
1720            raise Exception("Attempting to call `unnest` on an empty list")
1721        output = []
1722        for i in data:
1723            if isinstance(i, list):
1724                output += i
1725            else:
1726                output.append(i)
1727        return output
1728
1729    def zip(self, a: list, b: list):
1730        """
1731        Function:
1732
1733        - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
1734
1735        Requires:
1736
1737        - `a`:
1738            - Type: list
1739            - What: List of items to appear in new list first
1740        - `b`:
1741            - Type: list
1742            - What: List of items to appear in new list second
1743
1744        Example:
1745
1746        ```
1747        a=['a','b']
1748        b=[1,2]
1749        pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1750        ```
1751        """
1752        return list(map(list, zip(a, b)))
1753
1754    def zipObj(self, a: list, b: list):
1755        """
1756        Function:
1757
1758        - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
1759        - The first list represents keys and the second values
1760
1761        Requires:
1762
1763        - `a`:
1764            - Type: list
1765            - What: List of items to appear in new list first
1766        - `b`:
1767            - Type: list
1768            - What: List of items to appear in new list second
1769
1770        Example:
1771
1772        ```
1773        a=['a','b']
1774        b=[1,2]
1775        pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}
1776        ```
1777        """
1778        return dict(zip(a, b))
@pamda_wrappers.typed_curry_wrap
@pamda_wrappers.classmethod_wrap
class pamda(pamda.pamda_utils.pamda_utils):
  17@pamda_wrappers.typed_curry_wrap
  18@pamda_wrappers.classmethod_wrap
  19class pamda(pamda_utils):
  20    def accumulate(self, fn, initial_accumulator, data: list):
  21        """
  22        Function:
  23
  24        - Returns an accumulated list of items by iterating a function starting with an accumulator over a list
  25
  26        Requires:
  27
  28        - `fn`:
  29            - Type: function | method
  30            - What: The function or method to reduce
  31            - Note: This function should have an arity of 2 (take two inputs)
  32            - Note: The first input should take the accumulator value
  33            - Note: The second input should take the data value
  34        -`initial_accumulator`:
  35            - Type: any
  36            - What: The initial item to pass into the function when starting the accumulation process
  37        - `data`:
  38            - Type: list
  39            - What: The list of items to iterate over
  40
  41        Example:
  42
  43        ```
  44        data=[1,2,3,4]
  45        pamda.accumulate(
  46            fn=pamda.add,
  47            initial_accumulator=0,
  48            data=data
  49        )
  50        #=> [1,3,6,10]
  51
  52        ```
  53        """
  54        fn = self.curry(fn)
  55        if fn.__arity__ != 2:
  56            raise Exception("`fn` must have an arity of 2 (take two inputs)")
  57        if not len(data) > 0:
  58            raise Exception(
  59                "`data` has a length of 0, however it must have a length of at least 1"
  60            )
  61        acc = initial_accumulator
  62        out = []
  63        for i in data:
  64            acc = fn(acc, i)
  65            out.append(acc)
  66        return out
  67
  68    def add(self, a: int | float, b: int | float):
  69        """
  70        Function:
  71
  72        - Adds two numbers
  73
  74        Requires:
  75
  76        - `a`:
  77            - Type: int | float
  78            - What: The first number to add
  79        - `b`:
  80            - Type: int | float
  81            - What: The second number to add
  82
  83        Example:
  84
  85        ```
  86        pamda.add(1, 2) #=> 3
  87        ```
  88        """
  89        return a + b
  90
  91    def adjust(self, index: int, fn, data: list):
  92        """
  93        Function:
  94
  95        - Adjusts an item in a list by applying a function to it
  96
  97        Requires:
  98
  99        - `index`:
 100            - Type: int
 101            - What: The 0 based index of the item in the list to adjust
 102            - Note: Indicies are accepted
 103            - Note: If the index is out of range, picks the (-)first / (+)last item
 104        - `fn`:
 105            - Type: function | method
 106            - What: The function to apply the index item to
 107            - Note: This is automatically curried
 108        - `data`:
 109            - Type: list
 110            - What: The list to adjust
 111
 112        Example:
 113
 114        ```
 115        data=[1,5,9]
 116        pamda.adjust(
 117            index=1,
 118            fn=pamda.inc,
 119            data=data
 120        ) #=> [1,6,9]
 121        ```
 122        """
 123        fn = self.curry(fn)
 124        index = self.clamp(-len(data), len(data) - 1, index)
 125        data[index] = fn(data[index])
 126        return data
 127
 128    def assocPath(self, path: list | str | int | tuple, value, data: dict):
 129        """
 130        Function:
 131
 132        - Ensures a path exists within a nested dictionary
 133        - Note: This updates the object in place, but also returns the object
 134
 135        Requires:
 136
 137        - `path`:
 138            - Type: list[str | int | tuple] | str | int | tuple
 139            - What: The path to check
 140            - Note: If a string is passed, assumes a single item path list with that string
 141        - `value`:
 142            - Type: any
 143            - What: The value to appropriate to the end of the path
 144        - `data`:
 145            - Type: dict
 146            - What: A dictionary in which to associate the given value to the given path
 147
 148        Example:
 149
 150        ```
 151        data={'a':{'b':1}}
 152        pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}}
 153        ```
 154        """
 155        if not isinstance(path, list):
 156            path = [path]
 157        reduce(__getForceDict__, path[:-1], data).__setitem__(path[-1], value)
 158        return data
 159
 160    def assocPathComplex(
 161        self, default, default_fn, path: list | int | float | tuple, data: dict
 162    ):
 163        """
 164        Function:
 165
 166        - Ensures a path exists within a nested dictionary
 167        - Note: This updates the object in place, but also returns the object
 168
 169        Requires:
 170
 171        - `default`:
 172            - Type: any
 173            - What: The default item to add to a path that does not yet exist
 174        - `default_fn`:
 175            - Type: function | method
 176            - What: A unary (single input) function that takes in the current path item (or default) and adjusts it
 177            - Example: `lambda x: x` # Returns the value in the dict or the default value if none was present
 178        - `path`:
 179            - Type: list[str | int | tuple] | str | int | tuple
 180            - What: The path to check
 181        - `data`:
 182            - Type: dict
 183            - What: A dictionary to check if the path exists
 184
 185        Example:
 186
 187        ```
 188        data={'a':{'b':1}}
 189        pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}}
 190        ```
 191        """
 192        if self.getArity(default_fn) != 1:
 193            raise Exception(
 194                "`assocPathComplex` `default_fn` must be an unary (single input) function."
 195            )
 196        if not isinstance(path, list):
 197            path = [path]
 198        path_object = reduce(__getForceDict__, path[:-1], data)
 199        path_object.__setitem__(
 200            path[-1], default_fn(path_object.get(path[-1], default))
 201        )
 202        return data
 203
 204    def asyncKill(self, fn: curry_obj):
 205        """
 206        Function:
 207
 208        - Kills an asynchronous function that is currently running
 209        - Returns:
 210            - `None` if the function has not yet finished running
 211            - The result of the function if it has finished running
 212
 213        Requires:
 214
 215        - `fn`:
 216            - Type: thunkified function | thunkified method
 217            - What: The function or method to run asychronously
 218            - Note: The supplied `fn` must already be asynchronously running
 219
 220        Notes:
 221
 222        - See also `asyncRun` and `asyncWait`
 223        - A thunkified function currently running asynchronously can call `asyncKill` on itself
 224        - If a function has already finished running, calling `asyncKill` on it will have no effect
 225        - `asyncKill` does not kill threads that are sleeping (EG: `time.sleep`), but will kill the thread once the sleep is finished
 226
 227        Example:
 228
 229        ```
 230        import time
 231        from pamda import pamda
 232
 233        @pamda.thunkify
 234        def test(name, wait):
 235            waited = 0
 236            while waited < wait:
 237                time.sleep(1)
 238                waited += 1
 239                print(f'{name} has waited {waited} seconds')
 240
 241        async_test = pamda.asyncRun(test('a',3))
 242        time.sleep(1)
 243        pamda.asyncKill(async_test)
 244        # Alternatively:
 245        # async_test.asyncKill()
 246        ```
 247        """
 248        return fn.asyncKill()
 249
 250    def asyncRun(self, fn: curry_obj):
 251        """
 252        Function:
 253
 254        - Runs the supplied function asychronously
 255
 256        Requires:
 257
 258        - `fn`:
 259            - Type: thunkified function | thunkified method
 260            - What: The function or method to run asychronously
 261            - Note: The supplied `fn` must have an arity of 0
 262
 263        Notes:
 264
 265        - To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling `asyncRun` on it
 266        - To get the results of an `asyncRun` call `asyncWait`
 267        - To kill an `asyncRun` mid process call `asyncKill`
 268        - A thunkified function with arity of 0 can call `asyncRun` on itself
 269
 270        Examples:
 271
 272        Input:
 273        ```
 274        import time
 275
 276        @pamda.thunkify
 277        def test(name, wait):
 278            print(f'{name} start')
 279            time.sleep(wait)
 280            print(f'{name} end')
 281
 282        async_test = pamda.asyncRun(test('a',2))
 283        sync_test = test('b',1)()
 284        ```
 285        Output:
 286        ```
 287        a start
 288        b start
 289        b end
 290        a end
 291        ```
 292
 293
 294        Input:
 295        ```
 296        import time
 297
 298        @pamda.thunkify
 299        def test(name, wait):
 300            time.sleep(wait)
 301            return f"{name}: {wait}"
 302
 303        async_test = pamda.asyncRun(test('a',2))
 304        print(async_test.asyncWait()) #=> a: 2
 305        ```
 306
 307
 308        Input:
 309        ```
 310        import time
 311
 312        @pamda.thunkify
 313        def test(name, wait):
 314            time.sleep(wait)
 315            return f"{name}: {wait}"
 316
 317        async_test = test('a',2).asyncRun()
 318        print(async_test.asyncWait()) #=> a: 2
 319        ```
 320        """
 321        return fn.asyncRun()
 322
 323    def asyncWait(self, fn: curry_obj):
 324        """
 325        Function:
 326
 327        - Waits for a supplied function (if needed) and returns the results
 328
 329        Requires:
 330
 331        - `fn`:
 332            - Type: function | method
 333            - What: The function or method for which to wait
 334            - Note: The supplied `fn` must have previously called `asyncRun`
 335
 336        Notes:
 337
 338        - A thunkified function that has called `asyncRun` can call `asyncWait` on itself
 339
 340        Examples:
 341
 342        ```
 343        import time
 344
 345        @pamda.thunkify
 346        def test(name, wait):
 347            time.sleep(wait)
 348            return f"{name}: {wait}"
 349
 350        async_test = pamda.asyncRun(test('a',2))
 351        print(pamda.asyncWait(async_test)) #=> a: 2
 352        ```
 353
 354
 355        ```
 356        import time
 357
 358        @pamda.thunkify
 359        def test(name, wait):
 360            time.sleep(wait)
 361            return f"{name}: {wait}"
 362
 363        async_test = pamda.asyncRun(test('a',2))
 364        print(async_test.asyncWait()) #=> a: 2
 365        ```
 366        """
 367        return fn.asyncWait()
 368
 369    def clamp(self, minimum: int | float, maximum: int | float, a: int | float):
 370        """
 371        Function:
 372
 373        - Forces data to be within minimum and maximum
 374
 375        Requires:
 376
 377        - `minimum`:
 378            - Type: int | float
 379            - What: The minimum number
 380        - `maximum`:
 381            - Type: int | float
 382            - What: The maximum number
 383        - `a`:
 384            - Type: int | float
 385            - What: The number to clamp
 386
 387        Example:
 388
 389        ```
 390        pamda.clamp(1, 3, 2) #=> 2
 391        pamda.clamp(1, 3, 5) #=> 3
 392        ```
 393        """
 394        return min(max(a, minimum), maximum)
 395
 396    def curry(self, fn):
 397        """
 398        Function:
 399
 400        - Curries a function such that inputs can be added interatively
 401
 402        Requires:
 403
 404        - `fn`:
 405            - Type: function | method
 406            - What: The function or method to curry
 407            - Note: Class methods auto apply self during curry
 408
 409        Notes:
 410
 411        - Once curried, the function | method becomes a curry_obj object
 412        - The initial function is only called once all inputs are passed
 413
 414
 415        Examples:
 416
 417        ```
 418        curriedZip=pamda.curry(pamda.zip)
 419        curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]]
 420
 421        # Curried functions can be thunkified at any time
 422        # See also thunkify
 423        zipThunk=curriedZip.thunkify()(['a','b'])([1,2])
 424        zipThunk() #=> [['a',1],['b',2]]
 425        ```
 426
 427        ```
 428        def myFunction(a,b,c):
 429            return [a,b,c]
 430
 431        curriedMyFn=pamda.curry(myFunction)
 432
 433        curriedMyFn(1,2,3) #=> [1,2,3]
 434        curriedMyFn(1)(2,3) #=> [1,2,3]
 435
 436        x=curriedMyFn(1)(2)
 437        x(3) #=> [1,2,3]
 438        x(4) #=> [1,2,4]
 439
 440
 441        ```
 442        """
 443        if fn.__dict__.get("__isCurried__"):
 444            return fn()
 445        return curry_obj(fn)
 446
 447    def curryTyped(self, fn):
 448        """
 449        Function:
 450
 451        - Curries a function such that inputs can be added interatively and function annotations are type checked at runtime
 452
 453        Requires:
 454
 455        - `fn`:
 456            - Type: function | method
 457            - What: The function or method to curry
 458            - Note: Class methods auto apply self during curry
 459
 460        Notes:
 461
 462        - Once curried, the function | method becomes a curry_obj object
 463        - The initial function is only called once all inputs are passed
 464
 465
 466        Examples:
 467
 468        ```
 469        @pamda.curryTyped
 470        def add(a:int,b:int):
 471            return a+b
 472
 473        add(1)(1) #=> 2
 474        add(1)(1.5) #=> Raises type exception
 475        ```
 476        """
 477        if fn.__dict__.get("__isCurried__"):
 478            return fn().typeEnforce()
 479        return curry_obj(fn).typeEnforce()
 480
 481    def dec(self, a: int | float):
 482        """
 483        Function:
 484
 485        - Decrements a number by one
 486
 487        Requires:
 488
 489        - `a`:
 490            - Type: int | float
 491            - What: The number to decrement
 492
 493        Example:
 494
 495        ```
 496        pamda.dec(42) #=> 41
 497        ```
 498        """
 499        if not isinstance(a, (int, float)):
 500            raise Exception("`a` must be an `int` or a `float`")
 501        return a - 1
 502
 503    def difference(self, a: list, b: list):
 504        """
 505        Function:
 506
 507        - Combines two lists into a list of no duplicate items present in the first list but not the second
 508
 509        Requires:
 510
 511        - `a`:
 512            - Type: list
 513            - What: List of items in which to look for a difference
 514        - `b`:
 515            - Type: list
 516            - What: List of items in which to compare when looking for the difference
 517
 518        Example:
 519
 520        ```
 521        a=['a','b']
 522        b=['b','c']
 523        pamda.difference(a=a, b=b) #=> ['a']
 524        pamda.difference(a=b, b=a) #=> ['c']
 525        ```
 526        """
 527        return list(set(a).difference(set(b)))
 528
 529    def dissocPath(self, path: list | str | int | tuple, data: dict):
 530        """
 531        Function:
 532
 533        - Removes the value at the end of a path within a nested dictionary
 534        - Note: This updates the object in place, but also returns the object
 535
 536        Requires:
 537
 538        - `path`:
 539            - Type: list of strs | str
 540            - What: The path to remove from the dictionary
 541            - Note: If a string is passed, assumes a single item path list with that string
 542        - `data`:
 543            - Type: dict
 544            - What: A dictionary with a path to be removed
 545
 546        Example:
 547
 548        ```
 549        data={'a':{'b':{'c':0,'d':1}}}
 550        pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}}
 551        ```
 552        """
 553        if not isinstance(path, list):
 554            path = [path]
 555        if not self.hasPath(path=path, data=data):
 556            raise Exception("Path does not exist")
 557        else:
 558            reduce(__getForceDict__, path[:-1], data).pop(path[-1])
 559        return data
 560
 561    def flatten(self, data: list):
 562        """
 563        Function:
 564
 565        - Flattens a list of lists of lists ... into a single list depth first
 566
 567        Requires:
 568
 569        - `data`:
 570            - Type: list of lists
 571            - What: The list of lists to reduce to a single list
 572        Example:
 573
 574        ```
 575        data=[['a','b'],[1,[2]]]
 576        pamda.flatten(data=data) #=> ['a','b',1,2]
 577        ```
 578        """
 579
 580        def iter_flatten(data):
 581            out = []
 582            for i in data:
 583                if isinstance(i, list):
 584                    out.extend(iter_flatten(i))
 585                else:
 586                    out.append(i)
 587            return out
 588
 589        return iter_flatten(data)
 590
 591    def flip(self, fn):
 592        """
 593        Function:
 594
 595        - Returns a new function equivalent to the supplied function except that the first two inputs are flipped
 596
 597        Requires:
 598
 599        - `fn`:
 600            - Type: function | method
 601            - What: The function or method to flip
 602            - Note: This function must have an arity of at least 2 (take two inputs)
 603            - Note: Only args are flipped, kwargs are passed as normal
 604
 605        Notes:
 606
 607        - Input functions are not flipped in place
 608        - The returned function is a flipped version of the input function
 609        - A curried function can be flipped in place by calling fn.flip()
 610        - A function can be flipped multiple times:
 611            - At each flip, the first and second inputs for the function as it is currently curried are switched
 612            - Flipping a function two times before adding an input will return the initial value
 613
 614        Examples:
 615
 616        ```
 617        def concat(a,b,c,d):
 618            return str(a)+str(b)+str(c)+str(d)
 619
 620        flip_concat=pamda.flip(concat)
 621
 622        concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
 623        flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
 624        ```
 625
 626        ```
 627        @pamda.curry
 628        def concat(a,b,c,d):
 629            return str(a)+str(b)+str(c)+str(d)
 630
 631        concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
 632
 633        concat.flip()
 634
 635        concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
 636        ```
 637
 638        ```
 639        @pamda.curry
 640        def concat(a,b,c,d):
 641            return str(a)+str(b)+str(c)+str(d)
 642
 643        a=pamda.flip(concat)('fi-')
 644        b=pamda.flip(a)('fo-')
 645        c=pamda.flip(b)('fum')
 646        c('fe-') #=> 'fe-fi-fo-fum'
 647        ```
 648
 649        ```
 650        def concat(a,b,c,d):
 651            return str(a)+str(b)+str(c)+str(d)
 652
 653        a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum')
 654        a('fe-') #=> 'fe-fi-fo-fum'
 655        ```
 656        """
 657        fn = self.curry(fn)
 658        return fn.flip()
 659
 660    def getArity(self, fn):
 661        """
 662        Function:
 663
 664        - Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried)
 665
 666        Requires:
 667
 668        - `fn`:
 669            - Type: function | method
 670            - What: The function or method to get the arity of
 671            - Note: Class methods remove one arity to account for self
 672
 673        Examples:
 674
 675        ```
 676        pamda.getArity(pamda.zip) #=> 2
 677        curriedZip=pamda.curry(pamda.zip)
 678        ABCuriedZip=curriedZip(['a','b'])
 679        pamda.getArity(ABCuriedZip) #=> 1
 680        ```
 681        """
 682        fn = self.curry(fn)
 683        return fn.__arity__
 684
 685    def groupBy(self, fn, data: list):
 686        """
 687        Function:
 688
 689        - Splits a list into a dictionary of sublists keyed by the return string of a provided function
 690
 691        Requires:
 692
 693        - `fn`:
 694            - Type: function | method
 695            - What: The function or method to group by
 696            - Note: Must return a string (or other hashable object)
 697            - Note: This function must be unary (take one input)
 698            - Note: This function is applied to each item in the list recursively
 699        - `data`:
 700            - Type: list
 701            - What: List of items to apply the function to and then group by the results
 702
 703        Examples:
 704
 705        ```
 706        def getGrade(item):
 707            score=item['score']
 708            if score>90:
 709                return 'A'
 710            elif score>80:
 711                return 'B'
 712            elif score>70:
 713                return 'C'
 714            elif score>60:
 715                return 'D'
 716            else:
 717                return 'F'
 718
 719        data=[
 720            {'name':'Connor', 'score':75},
 721            {'name':'Fred', 'score':79},
 722            {'name':'Joe', 'score':84},
 723        ]
 724        pamda.groupBy(getGrade,data)
 725        #=>{
 726        #=>    'B':[{'name':'Joe', 'score':84}]
 727        #=>    'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}]
 728        #=>}
 729        ```
 730        """
 731        curried_fn = self.curry(fn)
 732        if curried_fn.__arity__ != 1:
 733            raise Exception(
 734                "groupBy `fn` must only take one parameter as its input"
 735            )
 736        return __groupByHashable__(fn=fn, data=data)
 737
 738    def groupKeys(self, keys: list, data: list):
 739        """
 740        Function:
 741
 742        - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys
 743
 744        Requires:
 745
 746        - `keys`:
 747            - Type: list of strs
 748            - What: The keys to group by
 749        - `data`:
 750            - Type: list of dicts
 751            - What: List of dictionaries with which to match keys
 752
 753        Examples:
 754
 755        ```
 756        data=[
 757            {'color':'red', 'size':9, 'shape':'ball'},
 758            {'color':'red', 'size':10, 'shape':'ball'},
 759            {'color':'green', 'size':11, 'shape':'ball'},
 760            {'color':'green', 'size':12, 'shape':'square'}
 761        ]
 762        pamda.groupKeys(['color','shape'],data)
 763        #=> [
 764        #=>     [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}],
 765        #=>     [{'color': 'green', 'size': 11, 'shape': 'ball'}],
 766        #=>     [{'color': 'green', 'size': 12, 'shape': 'square'}]
 767        #=> ]
 768        ```
 769        """
 770
 771        def key_fn(item):
 772            return tuple([item[key] for key in keys])
 773
 774        return list(__groupByHashable__(key_fn, data).values())
 775
 776    def groupWith(self, fn, data: list):
 777        """
 778        Function:
 779
 780        - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function
 781
 782        Requires:
 783
 784        - `fn`:
 785            - Type: function | method
 786            - What: The function or method to groub with
 787            - Note: Must return a boolean value
 788            - Note: This function must have an arity of two (take two inputs)
 789            - Note: This function is applied to each item plus the next adjacent item in the list recursively
 790        - `data`:
 791            - Type: list
 792            - What: List of items to apply the function to and then group the results
 793
 794        Examples:
 795
 796        ```
 797        def areEqual(a,b):
 798            return a==b
 799
 800        data=[1,2,3,1,1,2,2,3,3,3]
 801        pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]]
 802        ```
 803        """
 804        curried_fn = self.curry(fn)
 805        if curried_fn.__arity__ != 2:
 806            raise Exception("groupWith `fn` must take exactly two parameters")
 807        previous = data[0]
 808        output = []
 809        sublist = [previous]
 810        for i in data[1:]:
 811            if fn(i, previous):
 812                sublist.append(i)
 813            else:
 814                output.append(sublist)
 815                sublist = [i]
 816            previous = i
 817        output.append(sublist)
 818        return output
 819
 820    def hasPath(self, path: list | str, data: dict):
 821        """
 822        Function:
 823
 824        - Checks if a path exists within a nested dictionary
 825
 826        Requires:
 827
 828        - `path`:
 829            - Type: list of strs | str
 830            - What: The path to check
 831            - Note: If a string is passed, assumes a single item path list with that string
 832        - `data`:
 833            - Type: dict
 834            - What: A dictionary to check if the path exists
 835
 836        Example:
 837
 838        ```
 839        data={'a':{'b':1}}
 840        pamda.hasPath(path=['a','b'], data=data) #=> True
 841        pamda.hasPath(path=['a','d'], data=data) #=> False
 842        ```
 843        """
 844        if isinstance(path, str):
 845            path = [path]
 846        try:
 847            reduce(lambda x, y: x[y], path, data)
 848            return True
 849        except (KeyError, IndexError, TypeError):
 850            return False
 851
 852    def hardRound(self, decimal_places: int, a: int | float):
 853        """
 854        Function:
 855
 856        - Rounds to a set number of decimal places regardless of floating point math in python
 857
 858        Requires:
 859
 860        - `decimal_places`:
 861            - Type: int
 862            - What: The number of decimal places to round to
 863            - Default: 0
 864            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
 865        - `a`:
 866            - Type: int | float
 867            - What: The number to round
 868
 869        Example:
 870
 871        ```
 872        a=12.345
 873        pamda.hardRound(1,a) #=> 12.3
 874        pamda.hardRound(-1,a) #=> 10
 875        ```
 876        """
 877        return int(a * (10**decimal_places) + 0.5) / (10**decimal_places)
 878
 879    def head(self, data: list | str):
 880        """
 881        Function:
 882
 883        - Picks the first item out of a list or string
 884
 885        Requires:
 886
 887        - `data`:
 888            - Type: list | str
 889            - What: A list or string
 890
 891        Example:
 892
 893        ```
 894        data=['fe','fi','fo','fum']
 895        pamda.first(
 896            data=data
 897        ) #=> fe
 898        ```
 899        """
 900        if not isinstance(data, (list, str)):
 901            raise Exception("`head` can only be called on a `str` or a `list`")
 902        if not len(data) > 0:
 903            raise Exception("Attempting to call `head` on an empty list or str")
 904        return data[0]
 905
 906    def inc(self, a: int | float):
 907        """
 908        Function:
 909
 910        - Increments a number by one
 911
 912        Requires:
 913
 914        - `a`:
 915            - Type: int | float
 916            - What: The number to increment
 917
 918        Example:
 919
 920        ```
 921        pamda.inc(42) #=> 43
 922        ```
 923        """
 924        if not isinstance(a, (int, float)):
 925            raise Exception("`a` must be an `int` or a `float`")
 926        return a + 1
 927
 928    def intersection(self, a: list, b: list):
 929        """
 930        Function:
 931
 932        - Combines two lists into a list of no duplicates composed of those elements common to both lists
 933
 934        Requires:
 935
 936        - `a`:
 937            - Type: list
 938            - What: List of items in which to look for an intersection
 939        - `b`:
 940            - Type: list
 941            - What: List of items in which to look for an intersection
 942
 943        Example:
 944
 945        ```
 946        a=['a','b']
 947        b=['b','c']
 948        pamda.intersection(a=a, b=b) #=> ['b']
 949        ```
 950        """
 951        return list(set(a).intersection(set(b)))
 952
 953    def map(self, fn, data: list | dict):
 954        """
 955        Function:
 956
 957        - Maps a function over a list or a dictionary
 958
 959        Requires:
 960
 961        - `fn`:
 962            - Type: function | method
 963            - What: The function or method to map over the list or dictionary
 964            - Note: This function should have an arity of 1
 965        - `data`:
 966            - Type: list | dict
 967            - What: The list or dict of items to map the function over
 968
 969        Examples:
 970
 971        ```
 972        data=[1,2,3]
 973        pamda.map(
 974            fn=pamda.inc,
 975            data=data
 976        )
 977        #=> [2,3,4]
 978        ```
 979
 980        ```
 981        data={'a':1,'b':2,'c':3}
 982        pamda.map(
 983            fn=pamda.inc,
 984            data=data
 985        )
 986        #=> {'a':2,'b':3,'c':4}
 987        ```
 988
 989        """
 990        # TODO: Check for efficiency gains
 991        fn = self.curry(fn)
 992        if fn.__arity__ != 1:
 993            raise Exception("`map` `fn` must be unary (take one input)")
 994        if not len(data) > 0:
 995            raise Exception(
 996                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
 997            )
 998        if isinstance(data, dict):
 999            return {key: fn(value) for key, value in data.items()}
1000        else:
1001            return [fn(i) for i in data]
1002
1003    def mean(self, data: list):
1004        """
1005        Function:
1006
1007        - Calculates the mean of a given list
1008
1009        Requires:
1010
1011        - `data`:
1012            - Type: list of (floats | ints)
1013            - What: The list with wich to calculate the mean
1014            - Note: If the length of this list is 0, returns None
1015
1016        Example:
1017
1018        ```
1019        data=[1,2,3]
1020        pamda.mean(data=data)
1021        #=> 2
1022        ```
1023
1024        ```
1025        data=[]
1026        pamda.mean(data=data)
1027        #=> None
1028        ```
1029        """
1030        if len(data) == 0:
1031            return None
1032        return sum(data) / len(data)
1033
1034    def median(self, data: list):
1035        """
1036        Function:
1037
1038        - Calculates the median of a given list
1039        - If the length of the list is even, calculates the mean of the two central values
1040
1041        Requires:
1042
1043        - `data`:
1044            - Type: list of (floats | ints)
1045            - What: The list with wich to calculate the mean
1046            - Note: If the length of this list is 0, returns None
1047
1048        Examples:
1049
1050        ```
1051        data=[7,2,8,9]
1052        pamda.median(data=data)
1053        #=> 7.5
1054        ```
1055
1056        ```
1057        data=[7,8,9]
1058        pamda.median(data=data)
1059        #=> 8
1060        ```
1061
1062        ```
1063        data=[]
1064        pamda.median(data=data)
1065        #=> None
1066        ```
1067        """
1068        if not isinstance(data, (list)):
1069            raise Exception("`median` `data` must be a list")
1070        length = len(data)
1071        if length == 0:
1072            return None
1073        data = sorted(data)
1074        if length % 2 == 0:
1075            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1076        return data[int(length / 2)]
1077
1078    def mergeDeep(self, update_data, data):
1079        """
1080        Function:
1081
1082        - Recursively merges two nested dictionaries keeping all keys at each layer
1083        - Values from `update_data` are used when keys are present in both dictionaries
1084
1085        Requires:
1086
1087        - `update_data`:
1088            - Type: any
1089            - What: The new data that will take precedence during merging
1090        - `data`:
1091            - Type: any
1092            - What: The original data that will be merged into
1093
1094        Example:
1095
1096        ```
1097        data={'a':{'b':{'c':'d'},'e':'f'}}
1098        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1099        pamda.mergeDeep(
1100            update_data=update_data,
1101            data=data
1102        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1103        ```
1104        """
1105        return __mergeDeep__(update_data, data)
1106
1107    def nest(self, path_keys: list, value_key: str, data: list):
1108        """
1109        Function:
1110
1111        - Nests a list of dictionaries into a nested dictionary
1112        - Similar items are appended to a list in the end of the nested dictionary
1113
1114        Requires:
1115
1116        - `path_keys`:
1117            - Type: list of strs
1118            - What: The variables to pull from each item in data
1119            - Note: Used to build out the nested dicitonary
1120            - Note: Order matters as the nesting occurs in order of variable
1121        - `value_key`:
1122            - Type: str
1123            - What: The variable to add to the list at the end of the nested dictionary path
1124        - `data`:
1125            - Type: list of dicts
1126            - What: A list of dictionaries to use for nesting purposes
1127
1128        Example:
1129
1130        ```
1131        data=[
1132            {'x_1':'a','x_2':'b', 'output':'c'},
1133            {'x_1':'a','x_2':'b', 'output':'d'},
1134            {'x_1':'a','x_2':'e', 'output':'f'}
1135        ]
1136        pamda.nest(
1137            path_keys=['x_1','x_2'],
1138            value_key='output',
1139            data=data
1140        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1141        ```
1142        """
1143        if not isinstance(data, list):
1144            raise Exception("Attempting to `nest` an object that is not a list")
1145        if len(data) == 0:
1146            raise Exception("Attempting to `nest` from an empty list")
1147        nested_output = {}
1148        for item in self.groupKeys(keys=path_keys, data=data):
1149            nested_output = __assocPath__(
1150                path=__getKeyValues__(path_keys, item[0]),
1151                value=[i.get(value_key) for i in item],
1152                data=nested_output,
1153            )
1154        return nested_output
1155
1156    def nestItem(self, path_keys: list, data: list):
1157        """
1158        Function:
1159
1160        - Nests a list of dictionaries into a nested dictionary
1161        - Similar items are appended to a list in the end of the nested dictionary
1162        - Similar to `nest`, except no values are plucked for the aggregated list
1163
1164        Requires:
1165
1166        - `path_keys`:
1167            - Type: list of strs
1168            - What: The variables to pull from each item in data
1169            - Note: Used to build out the nested dicitonary
1170            - Note: Order matters as the nesting occurs in order of variable
1171        - `data`:
1172            - Type: list of dicts
1173            - What: A list of dictionaries to use for nesting purposes
1174
1175        Example:
1176
1177        ```
1178        data=[
1179            {'x_1':'a','x_2':'b'},
1180            {'x_1':'a','x_2':'b'},
1181            {'x_1':'a','x_2':'e'}
1182        ]
1183        pamda.nestItem
1184            path_keys=['x_1','x_2'],
1185            data=data
1186        )
1187        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1188
1189        ```
1190        """
1191        if not isinstance(data, list):
1192            raise Exception("Attempting to `nest` an object that is not a list")
1193        if len(data) == 0:
1194            raise Exception("Attempting to `nest` from an empty list")
1195        nested_output = {}
1196        for item in self.groupKeys(keys=path_keys, data=data):
1197            nested_output = __assocPath__(
1198                path=__getKeyValues__(path_keys, item[0]),
1199                value=item,
1200                data=nested_output,
1201            )
1202        return nested_output
1203
1204    def path(self, path: list | str, data: dict):
1205        """
1206        Function:
1207
1208        - Returns the value of a path within a nested dictionary or None if the path does not exist
1209
1210        Requires:
1211
1212        - `path`:
1213            - Type: list of strs | str
1214            - What: The path to pull given the data
1215            - Note: If a string is passed, assumes a single item path list with that string
1216        - `data`:
1217            - Type: dict
1218            - What: A dictionary to get the path from
1219
1220        Example:
1221
1222        ```
1223        data={'a':{'b':1}}
1224        pamda.path(path=['a','b'], data=data) #=> 1
1225        ```
1226        """
1227        if isinstance(path, str):
1228            path = [path]
1229        return __pathOr__(None, path, data)
1230
1231    def pathOr(self, default, path: list | str, data: dict):
1232        """
1233        Function:
1234
1235        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1236
1237        Requires:
1238
1239        - `default`:
1240            - Type: any
1241            - What: The object to return if the path does not exist
1242        - `path`:
1243            - Type: list of strs | str
1244            - What: The path to pull given the data
1245            - Note: If a string is passed, assumes a single item path list with that string
1246        - `data`:
1247            - Type: dict
1248            - What: A dictionary to get the path from
1249
1250        Example:
1251
1252        ```
1253        data={'a':{'b':1}}
1254        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1255        ```
1256        """
1257        if isinstance(path, str):
1258            path = [path]
1259        try:
1260            return reduce(lambda x, y: x[y], path, data)
1261        except (KeyError, IndexError, TypeError):
1262            return default
1263
1264    def pipe(self, fns: list, args: tuple, kwargs: dict):
1265        """
1266        Function:
1267
1268        - Pipes data through n functions in order (left to right composition) and returns the output
1269
1270        Requires:
1271
1272        - `fns`:
1273            - Type: list of (functions | methods)
1274            - What: The list of functions and methods to pipe the data through
1275            - Notes: The first function in the list can be any arity (accepting any number of inputs)
1276            - Notes: Any further function in the list can only be unary (single input)
1277            - Notes: A function can be curried, but is not required to be
1278            - Notes: You may opt to curry functions and add inputs to make them unary
1279        - `args`:
1280            - Type: tuple
1281            - What: a tuple of positional arguments to pass to the first function in `fns`
1282        - `kwargs`:
1283            - Type: dict
1284            - What: a dictionary of keyword arguments to pass to the first function in `fns`
1285
1286        Examples:
1287
1288        ```
1289        data=['abc','def']
1290        pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
1291        pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
1292        ```
1293
1294        ```
1295        data={'a':{'b':'c'}}
1296        curriedPath=pamda.curry(pamda.path)
1297        pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1298        ```
1299        """
1300        if len(fns) == 0:
1301            raise Exception("`fns` must be a list with at least one function")
1302        if self.getArity(fns[0]) == 0:
1303            raise Exception(
1304                "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0."
1305            )
1306        if not all([(self.getArity(fn) == 1) for fn in fns[1:]]):
1307            raise Exception(
1308                "Only the first function in `fns` can have n arity (accept n args). All other functions must have an arity of one (accepting one argument)."
1309            )
1310        out = fns[0](*args, **kwargs)
1311        for fn in fns[1:]:
1312            out = fn(out)
1313        return out
1314
1315    def pivot(self, data: list[dict] | dict[Any, list]):
1316        """
1317        Function:
1318
1319        - Pivots a list of dictionaries into a dictionary of lists
1320        - Pivots a dictionary of lists into a list of dictionaries
1321
1322        Requires:
1323
1324        - `data`:
1325            - Type: list of dicts | dict of lists
1326            - What: The data to pivot
1327            - Note: If a list of dictionaries is passed, all dictionaries must have the same keys
1328            - Note: If a dictionary of lists is passed, all lists must have the same length
1329
1330        Example:
1331
1332        ```
1333        data=[
1334            {'a':1,'b':2},
1335            {'a':3,'b':4}
1336        ]
1337        pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
1338
1339        data={'a':[1,3],'b':[2,4]}
1340        pamda.pivot(data=data)
1341        #=> [
1342        #=>     {'a':1,'b':2},
1343        #=>     {'a':3,'b':4}
1344        #=> ]
1345        ```
1346        """
1347        if isinstance(data, list):
1348            return {
1349                key: [record[key] for record in data] for key in data[0].keys()
1350            }
1351        else:
1352            return [
1353                {key: data[key][i] for key in data.keys()}
1354                for i in range(len(data[list(data.keys())[0]]))
1355            ]
1356
1357    def pluck(self, path: list | str, data: list):
1358        """
1359        Function:
1360
1361        - Returns the values of a path within a list of nested dictionaries
1362
1363        Requires:
1364
1365        - `path`:
1366            - Type: list of strs
1367            - What: The path to pull given the data
1368            - Note: If a string is passed, assumes a single item path list with that string
1369        - `data`:
1370            - Type: list of dicts
1371            - What: A list of dictionaries to get the path from
1372
1373        Example:
1374
1375        ```
1376        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1377        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1378        ```
1379        """
1380        if len(data) == 0:
1381            raise Exception("Attempting to pluck from an empty list")
1382        if isinstance(path, str):
1383            path = [path]
1384        return [__pathOr__(default=None, path=path, data=i) for i in data]
1385
1386    def pluckIf(self, fn, path: list | str, data: list):
1387        """
1388        Function:
1389
1390        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1391
1392        Requires:
1393
1394        - `fn`:
1395            - Type: function
1396            - What: A function to take in each item in data and return a boolean
1397            - Note: Only items that return true are plucked
1398            - Note: Should be a unary function (take one input)
1399        - `path`:
1400            - Type: list of strs
1401            - What: The path to pull given the data
1402            - Note: If a string is passed, assumes a single item path list with that string
1403        - `data`:
1404            - Type: list of dicts
1405            - What: A list of dictionary to get the path from
1406
1407        Example:
1408
1409        ```
1410
1411        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1412        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1413        ```
1414        """
1415        if len(data) == 0:
1416            raise Exception("Attempting to pluck from an empty list")
1417        curried_fn = self.curry(fn)
1418        if curried_fn.__arity__ != 1:
1419            raise Exception(
1420                "`pluckIf` `fn` must have an arity of 1 (take one input)"
1421            )
1422        if isinstance(path, str):
1423            path = [path]
1424        return [
1425            __pathOr__(default=None, path=path, data=i) for i in data if fn(i)
1426        ]
1427
1428    def project(self, keys: list[str], data: list[dict]):
1429        """
1430        Function:
1431
1432        - Returns a list of dictionaries with only the keys provided
1433        - Analogous to SQL's `SELECT` statement
1434
1435        Requires:
1436
1437        - `keys`:
1438            - Type: list of strs
1439            - What: The keys to select from each dictionary in the data list
1440        - `data`:
1441            - Type: list of dicts
1442            - What: The list of dictionaries to select from
1443
1444        Example:
1445
1446        ```
1447        data=[
1448            {'a':1,'b':2,'c':3},
1449            {'a':4,'b':5,'c':6}
1450        ]
1451        pamda.project(keys=['a','c'], data=data)
1452        #=> [
1453        #=>     {'a':1,'c':3},
1454        #=>     {'a':4,'c':6}
1455        #=> ]
1456        ```
1457        """
1458        return [{key: record[key] for key in keys} for record in data]
1459
1460    def props(self, keys: list[str], data: dict):
1461        """
1462        Function:
1463
1464        - Returns the values of a list of keys within a dictionary
1465
1466        Requires:
1467
1468        - `keys`:
1469            - Type: list of strs
1470            - What: The keys to pull given the data
1471        - `data`:
1472            - Type: dict
1473            - What: A dictionary to get the keys from
1474
1475        Example:
1476        ```
1477        data={'a':1,'b':2,'c':3}
1478        pamda.props(keys=['a','c'], data=data)
1479        #=> [1,3]
1480        ```
1481        """
1482        return [data[key] for key in keys]
1483
1484    def reduce(self, fn, initial_accumulator, data: list):
1485        """
1486        Function:
1487
1488        - Returns a single item by iterating a function starting with an accumulator over a list
1489
1490        Requires:
1491
1492        - `fn`:
1493            - Type: function | method
1494            - What: The function or method to reduce
1495            - Note: This function should have an arity of 2 (take two inputs)
1496            - Note: The first input should take the accumulator value
1497            - Note: The second input should take the data value
1498        -`initial_accumulator`:
1499            - Type: any
1500            - What: The initial item to pass into the function when starting the accumulation process
1501        - `data`:
1502            - Type: list
1503            - What: The list of items to iterate over
1504
1505        Example:
1506
1507        ```
1508        data=[1,2,3,4]
1509        pamda.reduce(
1510            fn=pamda.add,
1511            initial_accumulator=0,
1512            data=data
1513        )
1514        #=> 10
1515
1516        ```
1517        """
1518        fn = self.curry(fn)
1519        if fn.__arity__ != 2:
1520            raise Exception(
1521                "`reduce` `fn` must have an arity of 2 (take two inputs)"
1522            )
1523        if not isinstance(data, (list)):
1524            raise Exception("`reduce` `data` must be a list")
1525        if not len(data) > 0:
1526            raise Exception(
1527                "`reduce` `data` has a length of 0, however it must have a length of at least 1"
1528            )
1529        acc = initial_accumulator
1530        for i in data:
1531            acc = fn(acc, i)
1532        return acc
1533
1534    def safeDivide(self, denominator: int | float, a: int | float):
1535        """
1536        Function:
1537
1538        - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
1539
1540        Requires:
1541
1542        - `denominator`:
1543            - Type: int | float
1544            - What: The denominator
1545
1546        - `a`:
1547            - Type: int | float
1548            - What: The numerator
1549
1550        Example:
1551
1552        ```
1553        pamda.safeDivide(2,10) #=> 5
1554        pamda.safeDivide(0,10) #=> 10
1555        ```
1556        """
1557        return a / denominator if denominator != 0 else a
1558
1559    def safeDivideDefault(
1560        self,
1561        default_denominator: int | float,
1562        denominator: int | float,
1563        a: int | float,
1564    ):
1565        """
1566        Function:
1567
1568        - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
1569
1570        Requires:
1571
1572        - `default_denominator`:
1573            - Type: int | float
1574            - What: A non zero denominator to use if denominator is zero
1575            - Default: 1
1576        - `denominator`:
1577            - Type: int | float
1578            - What: The denominator
1579        - `a`:
1580            - Type: int | float
1581            - What: The numerator
1582
1583        Example:
1584
1585        ```
1586        pamda.safeDivideDefault(2,5,10) #=> 2
1587        pamda.safeDivideDefault(2,0,10) #=> 5
1588        ```
1589        """
1590        if default_denominator == 0:
1591            raise Exception(
1592                "`safeDivideDefault` `default_denominator` can not be 0"
1593            )
1594        return a / denominator if denominator != 0 else a / default_denominator
1595
1596    def symmetricDifference(self, a: list, b: list):
1597        """
1598        Function:
1599
1600        - Combines two lists into a list of no duplicates items present in one list but not the other
1601
1602        Requires:
1603
1604        - `a`:
1605            - Type: list
1606            - What: List of items in which to look for a difference
1607        - `b`:
1608            - Type: list
1609            - What: List of items in which to look for a difference
1610
1611        Example:
1612
1613        ```
1614        a=['a','b']
1615        b=['b','c']
1616        pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1617        ```
1618        """
1619        return list(set(a).difference(set(b))) + list(set(b).difference(set(a)))
1620
1621    def tail(self, data: list | str):
1622        """
1623        Function:
1624
1625        - Picks the last item out of a list or string
1626
1627        Requires:
1628
1629        - `data`:
1630            - Type: list | str
1631            - What: A list or string
1632
1633        Example:
1634
1635        ```
1636        data=['fe','fi','fo','fum']
1637        pamda.tail(
1638            data=data
1639        ) #=> fum
1640        ```
1641        """
1642        if not len(data) > 0:
1643            raise Exception("Attempting to call `tail` on an empty list or str")
1644        return data[-1]
1645
1646    def thunkify(self, fn):
1647        """
1648        Function:
1649
1650        - Creates a curried thunk out of a function
1651        - Evaluation of the thunk lazy and is delayed until called
1652
1653        Requires:
1654
1655        - `fn`:
1656            - Type: function | method
1657            - What: The function or method to thunkify
1658            - Note: Thunkified functions are automatically curried
1659            - Note: Class methods auto apply self during thunkify
1660
1661        Notes:
1662
1663        - Input functions are not thunkified in place
1664        - The returned function is a thunkified version of the input function
1665        - A curried function can be thunkified in place by calling fn.thunkify()
1666
1667        Examples:
1668
1669        ```
1670        def add(a,b):
1671            return a+b
1672
1673        addThunk=pamda.thunkify(add)
1674
1675        add(1,2) #=> 3
1676        addThunk(1,2)
1677        addThunk(1,2)() #=> 3
1678
1679        x=addThunk(1,2)
1680        x() #=> 3
1681        ```
1682
1683        ```
1684        @pamda.curry
1685        def add(a,b):
1686            return a+b
1687
1688        add(1,2) #=> 3
1689
1690        add.thunkify()
1691
1692        add(1,2)
1693        add(1,2)() #=> 3
1694        ```
1695        """
1696        fn = self.curry(fn)
1697        return fn.thunkify()
1698
1699    def unnest(self, data: list):
1700        """
1701        Function:
1702
1703        - Removes one level of depth for all items in a list
1704
1705        Requires:
1706
1707        - `data`:
1708            - Type: list
1709            - What: A list of items to unnest by one level
1710
1711        Examples:
1712
1713        ```
1714        data=['fe','fi',['fo',['fum']]]
1715        pamda.unnest(
1716            data=data
1717        ) #=> ['fe','fi','fo',['fum']]
1718        ```
1719        """
1720        if not len(data) > 0:
1721            raise Exception("Attempting to call `unnest` on an empty list")
1722        output = []
1723        for i in data:
1724            if isinstance(i, list):
1725                output += i
1726            else:
1727                output.append(i)
1728        return output
1729
1730    def zip(self, a: list, b: list):
1731        """
1732        Function:
1733
1734        - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
1735
1736        Requires:
1737
1738        - `a`:
1739            - Type: list
1740            - What: List of items to appear in new list first
1741        - `b`:
1742            - Type: list
1743            - What: List of items to appear in new list second
1744
1745        Example:
1746
1747        ```
1748        a=['a','b']
1749        b=[1,2]
1750        pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1751        ```
1752        """
1753        return list(map(list, zip(a, b)))
1754
1755    def zipObj(self, a: list, b: list):
1756        """
1757        Function:
1758
1759        - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
1760        - The first list represents keys and the second values
1761
1762        Requires:
1763
1764        - `a`:
1765            - Type: list
1766            - What: List of items to appear in new list first
1767        - `b`:
1768            - Type: list
1769            - What: List of items to appear in new list second
1770
1771        Example:
1772
1773        ```
1774        a=['a','b']
1775        b=[1,2]
1776        pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}
1777        ```
1778        """
1779        return dict(zip(a, b))
def accumulate(self, fn, initial_accumulator, data: list):
20    def accumulate(self, fn, initial_accumulator, data: list):
21        """
22        Function:
23
24        - Returns an accumulated list of items by iterating a function starting with an accumulator over a list
25
26        Requires:
27
28        - `fn`:
29            - Type: function | method
30            - What: The function or method to reduce
31            - Note: This function should have an arity of 2 (take two inputs)
32            - Note: The first input should take the accumulator value
33            - Note: The second input should take the data value
34        -`initial_accumulator`:
35            - Type: any
36            - What: The initial item to pass into the function when starting the accumulation process
37        - `data`:
38            - Type: list
39            - What: The list of items to iterate over
40
41        Example:
42
43        ```
44        data=[1,2,3,4]
45        pamda.accumulate(
46            fn=pamda.add,
47            initial_accumulator=0,
48            data=data
49        )
50        #=> [1,3,6,10]
51
52        ```
53        """
54        fn = self.curry(fn)
55        if fn.__arity__ != 2:
56            raise Exception("`fn` must have an arity of 2 (take two inputs)")
57        if not len(data) > 0:
58            raise Exception(
59                "`data` has a length of 0, however it must have a length of at least 1"
60            )
61        acc = initial_accumulator
62        out = []
63        for i in data:
64            acc = fn(acc, i)
65            out.append(acc)
66        return out

Function:

  • Returns an accumulated list of items by iterating a function starting with an accumulator over a list

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to reduce
    • Note: This function should have an arity of 2 (take two inputs)
    • Note: The first input should take the accumulator value
    • Note: The second input should take the data value -initial_accumulator:
    • Type: any
    • What: The initial item to pass into the function when starting the accumulation process
  • data:
    • Type: list
    • What: The list of items to iterate over

Example:

data=[1,2,3,4]
pamda.accumulate(
    fn=pamda.add,
    initial_accumulator=0,
    data=data
)
#=> [1,3,6,10]

def add(self, a: int | float, b: int | float):
68    def add(self, a: int | float, b: int | float):
69        """
70        Function:
71
72        - Adds two numbers
73
74        Requires:
75
76        - `a`:
77            - Type: int | float
78            - What: The first number to add
79        - `b`:
80            - Type: int | float
81            - What: The second number to add
82
83        Example:
84
85        ```
86        pamda.add(1, 2) #=> 3
87        ```
88        """
89        return a + b

Function:

  • Adds two numbers

Requires:

  • a:
    • Type: int | float
    • What: The first number to add
  • b:
    • Type: int | float
    • What: The second number to add

Example:

pamda.add(1, 2) #=> 3
def adjust(self, index: int, fn, data: list):
 91    def adjust(self, index: int, fn, data: list):
 92        """
 93        Function:
 94
 95        - Adjusts an item in a list by applying a function to it
 96
 97        Requires:
 98
 99        - `index`:
100            - Type: int
101            - What: The 0 based index of the item in the list to adjust
102            - Note: Indicies are accepted
103            - Note: If the index is out of range, picks the (-)first / (+)last item
104        - `fn`:
105            - Type: function | method
106            - What: The function to apply the index item to
107            - Note: This is automatically curried
108        - `data`:
109            - Type: list
110            - What: The list to adjust
111
112        Example:
113
114        ```
115        data=[1,5,9]
116        pamda.adjust(
117            index=1,
118            fn=pamda.inc,
119            data=data
120        ) #=> [1,6,9]
121        ```
122        """
123        fn = self.curry(fn)
124        index = self.clamp(-len(data), len(data) - 1, index)
125        data[index] = fn(data[index])
126        return data

Function:

  • Adjusts an item in a list by applying a function to it

Requires:

  • index:
    • Type: int
    • What: The 0 based index of the item in the list to adjust
    • Note: Indicies are accepted
    • Note: If the index is out of range, picks the (-)first / (+)last item
  • fn:
    • Type: function | method
    • What: The function to apply the index item to
    • Note: This is automatically curried
  • data:
    • Type: list
    • What: The list to adjust

Example:

data=[1,5,9]
pamda.adjust(
    index=1,
    fn=pamda.inc,
    data=data
) #=> [1,6,9]
def assocPath(self, path: list | str | int | tuple, value, data: dict):
128    def assocPath(self, path: list | str | int | tuple, value, data: dict):
129        """
130        Function:
131
132        - Ensures a path exists within a nested dictionary
133        - Note: This updates the object in place, but also returns the object
134
135        Requires:
136
137        - `path`:
138            - Type: list[str | int | tuple] | str | int | tuple
139            - What: The path to check
140            - Note: If a string is passed, assumes a single item path list with that string
141        - `value`:
142            - Type: any
143            - What: The value to appropriate to the end of the path
144        - `data`:
145            - Type: dict
146            - What: A dictionary in which to associate the given value to the given path
147
148        Example:
149
150        ```
151        data={'a':{'b':1}}
152        pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}}
153        ```
154        """
155        if not isinstance(path, list):
156            path = [path]
157        reduce(__getForceDict__, path[:-1], data).__setitem__(path[-1], value)
158        return data

Function:

  • Ensures a path exists within a nested dictionary
  • Note: This updates the object in place, but also returns the object

Requires:

  • path:
    • Type: list[str | int | tuple] | str | int | tuple
    • What: The path to check
    • Note: If a string is passed, assumes a single item path list with that string
  • value:
    • Type: any
    • What: The value to appropriate to the end of the path
  • data:
    • Type: dict
    • What: A dictionary in which to associate the given value to the given path

Example:

data={'a':{'b':1}}
pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}}
def assocPathComplex( self, default, default_fn, path: list | int | float | tuple, data: dict):
160    def assocPathComplex(
161        self, default, default_fn, path: list | int | float | tuple, data: dict
162    ):
163        """
164        Function:
165
166        - Ensures a path exists within a nested dictionary
167        - Note: This updates the object in place, but also returns the object
168
169        Requires:
170
171        - `default`:
172            - Type: any
173            - What: The default item to add to a path that does not yet exist
174        - `default_fn`:
175            - Type: function | method
176            - What: A unary (single input) function that takes in the current path item (or default) and adjusts it
177            - Example: `lambda x: x` # Returns the value in the dict or the default value if none was present
178        - `path`:
179            - Type: list[str | int | tuple] | str | int | tuple
180            - What: The path to check
181        - `data`:
182            - Type: dict
183            - What: A dictionary to check if the path exists
184
185        Example:
186
187        ```
188        data={'a':{'b':1}}
189        pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}}
190        ```
191        """
192        if self.getArity(default_fn) != 1:
193            raise Exception(
194                "`assocPathComplex` `default_fn` must be an unary (single input) function."
195            )
196        if not isinstance(path, list):
197            path = [path]
198        path_object = reduce(__getForceDict__, path[:-1], data)
199        path_object.__setitem__(
200            path[-1], default_fn(path_object.get(path[-1], default))
201        )
202        return data

Function:

  • Ensures a path exists within a nested dictionary
  • Note: This updates the object in place, but also returns the object

Requires:

  • default:
    • Type: any
    • What: The default item to add to a path that does not yet exist
  • default_fn:
    • Type: function | method
    • What: A unary (single input) function that takes in the current path item (or default) and adjusts it
    • Example: lambda x: x # Returns the value in the dict or the default value if none was present
  • path:
    • Type: list[str | int | tuple] | str | int | tuple
    • What: The path to check
  • data:
    • Type: dict
    • What: A dictionary to check if the path exists

Example:

data={'a':{'b':1}}
pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}}
def asyncKill(self, fn: pamda.pamda_curry.curry_obj):
204    def asyncKill(self, fn: curry_obj):
205        """
206        Function:
207
208        - Kills an asynchronous function that is currently running
209        - Returns:
210            - `None` if the function has not yet finished running
211            - The result of the function if it has finished running
212
213        Requires:
214
215        - `fn`:
216            - Type: thunkified function | thunkified method
217            - What: The function or method to run asychronously
218            - Note: The supplied `fn` must already be asynchronously running
219
220        Notes:
221
222        - See also `asyncRun` and `asyncWait`
223        - A thunkified function currently running asynchronously can call `asyncKill` on itself
224        - If a function has already finished running, calling `asyncKill` on it will have no effect
225        - `asyncKill` does not kill threads that are sleeping (EG: `time.sleep`), but will kill the thread once the sleep is finished
226
227        Example:
228
229        ```
230        import time
231        from pamda import pamda
232
233        @pamda.thunkify
234        def test(name, wait):
235            waited = 0
236            while waited < wait:
237                time.sleep(1)
238                waited += 1
239                print(f'{name} has waited {waited} seconds')
240
241        async_test = pamda.asyncRun(test('a',3))
242        time.sleep(1)
243        pamda.asyncKill(async_test)
244        # Alternatively:
245        # async_test.asyncKill()
246        ```
247        """
248        return fn.asyncKill()

Function:

  • Kills an asynchronous function that is currently running
  • Returns:
    • None if the function has not yet finished running
    • The result of the function if it has finished running

Requires:

  • fn:
    • Type: thunkified function | thunkified method
    • What: The function or method to run asychronously
    • Note: The supplied fn must already be asynchronously running

Notes:

  • See also asyncRun and asyncWait
  • A thunkified function currently running asynchronously can call asyncKill on itself
  • If a function has already finished running, calling asyncKill on it will have no effect
  • asyncKill does not kill threads that are sleeping (EG: time.sleep), but will kill the thread once the sleep is finished

Example:

import time
from pamda import pamda

@pamda.thunkify
def test(name, wait):
    waited = 0
    while waited < wait:
        time.sleep(1)
        waited += 1
        print(f'{name} has waited {waited} seconds')

async_test = pamda.asyncRun(test('a',3))
time.sleep(1)
pamda.asyncKill(async_test)
# Alternatively:
# async_test.asyncKill()
def asyncRun(self, fn: pamda.pamda_curry.curry_obj):
250    def asyncRun(self, fn: curry_obj):
251        """
252        Function:
253
254        - Runs the supplied function asychronously
255
256        Requires:
257
258        - `fn`:
259            - Type: thunkified function | thunkified method
260            - What: The function or method to run asychronously
261            - Note: The supplied `fn` must have an arity of 0
262
263        Notes:
264
265        - To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling `asyncRun` on it
266        - To get the results of an `asyncRun` call `asyncWait`
267        - To kill an `asyncRun` mid process call `asyncKill`
268        - A thunkified function with arity of 0 can call `asyncRun` on itself
269
270        Examples:
271
272        Input:
273        ```
274        import time
275
276        @pamda.thunkify
277        def test(name, wait):
278            print(f'{name} start')
279            time.sleep(wait)
280            print(f'{name} end')
281
282        async_test = pamda.asyncRun(test('a',2))
283        sync_test = test('b',1)()
284        ```
285        Output:
286        ```
287        a start
288        b start
289        b end
290        a end
291        ```
292
293
294        Input:
295        ```
296        import time
297
298        @pamda.thunkify
299        def test(name, wait):
300            time.sleep(wait)
301            return f"{name}: {wait}"
302
303        async_test = pamda.asyncRun(test('a',2))
304        print(async_test.asyncWait()) #=> a: 2
305        ```
306
307
308        Input:
309        ```
310        import time
311
312        @pamda.thunkify
313        def test(name, wait):
314            time.sleep(wait)
315            return f"{name}: {wait}"
316
317        async_test = test('a',2).asyncRun()
318        print(async_test.asyncWait()) #=> a: 2
319        ```
320        """
321        return fn.asyncRun()

Function:

  • Runs the supplied function asychronously

Requires:

  • fn:
    • Type: thunkified function | thunkified method
    • What: The function or method to run asychronously
    • Note: The supplied fn must have an arity of 0

Notes:

  • To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling asyncRun on it
  • To get the results of an asyncRun call asyncWait
  • To kill an asyncRun mid process call asyncKill
  • A thunkified function with arity of 0 can call asyncRun on itself

Examples:

Input:

import time

@pamda.thunkify
def test(name, wait):
    print(f'{name} start')
    time.sleep(wait)
    print(f'{name} end')

async_test = pamda.asyncRun(test('a',2))
sync_test = test('b',1)()

Output:

a start
b start
b end
a end

Input:

import time

@pamda.thunkify
def test(name, wait):
    time.sleep(wait)
    return f"{name}: {wait}"

async_test = pamda.asyncRun(test('a',2))
print(async_test.asyncWait()) #=> a: 2

Input:

import time

@pamda.thunkify
def test(name, wait):
    time.sleep(wait)
    return f"{name}: {wait}"

async_test = test('a',2).asyncRun()
print(async_test.asyncWait()) #=> a: 2
def asyncWait(self, fn: pamda.pamda_curry.curry_obj):
323    def asyncWait(self, fn: curry_obj):
324        """
325        Function:
326
327        - Waits for a supplied function (if needed) and returns the results
328
329        Requires:
330
331        - `fn`:
332            - Type: function | method
333            - What: The function or method for which to wait
334            - Note: The supplied `fn` must have previously called `asyncRun`
335
336        Notes:
337
338        - A thunkified function that has called `asyncRun` can call `asyncWait` on itself
339
340        Examples:
341
342        ```
343        import time
344
345        @pamda.thunkify
346        def test(name, wait):
347            time.sleep(wait)
348            return f"{name}: {wait}"
349
350        async_test = pamda.asyncRun(test('a',2))
351        print(pamda.asyncWait(async_test)) #=> a: 2
352        ```
353
354
355        ```
356        import time
357
358        @pamda.thunkify
359        def test(name, wait):
360            time.sleep(wait)
361            return f"{name}: {wait}"
362
363        async_test = pamda.asyncRun(test('a',2))
364        print(async_test.asyncWait()) #=> a: 2
365        ```
366        """
367        return fn.asyncWait()

Function:

  • Waits for a supplied function (if needed) and returns the results

Requires:

  • fn:
    • Type: function | method
    • What: The function or method for which to wait
    • Note: The supplied fn must have previously called asyncRun

Notes:

Examples:

import time

@pamda.thunkify
def test(name, wait):
    time.sleep(wait)
    return f"{name}: {wait}"

async_test = pamda.asyncRun(test('a',2))
print(pamda.asyncWait(async_test)) #=> a: 2
import time

@pamda.thunkify
def test(name, wait):
    time.sleep(wait)
    return f"{name}: {wait}"

async_test = pamda.asyncRun(test('a',2))
print(async_test.asyncWait()) #=> a: 2
def clamp(self, minimum: int | float, maximum: int | float, a: int | float):
369    def clamp(self, minimum: int | float, maximum: int | float, a: int | float):
370        """
371        Function:
372
373        - Forces data to be within minimum and maximum
374
375        Requires:
376
377        - `minimum`:
378            - Type: int | float
379            - What: The minimum number
380        - `maximum`:
381            - Type: int | float
382            - What: The maximum number
383        - `a`:
384            - Type: int | float
385            - What: The number to clamp
386
387        Example:
388
389        ```
390        pamda.clamp(1, 3, 2) #=> 2
391        pamda.clamp(1, 3, 5) #=> 3
392        ```
393        """
394        return min(max(a, minimum), maximum)

Function:

  • Forces data to be within minimum and maximum

Requires:

  • minimum:
    • Type: int | float
    • What: The minimum number
  • maximum:
    • Type: int | float
    • What: The maximum number
  • a:
    • Type: int | float
    • What: The number to clamp

Example:

pamda.clamp(1, 3, 2) #=> 2
pamda.clamp(1, 3, 5) #=> 3
def curry(self, fn):
396    def curry(self, fn):
397        """
398        Function:
399
400        - Curries a function such that inputs can be added interatively
401
402        Requires:
403
404        - `fn`:
405            - Type: function | method
406            - What: The function or method to curry
407            - Note: Class methods auto apply self during curry
408
409        Notes:
410
411        - Once curried, the function | method becomes a curry_obj object
412        - The initial function is only called once all inputs are passed
413
414
415        Examples:
416
417        ```
418        curriedZip=pamda.curry(pamda.zip)
419        curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]]
420
421        # Curried functions can be thunkified at any time
422        # See also thunkify
423        zipThunk=curriedZip.thunkify()(['a','b'])([1,2])
424        zipThunk() #=> [['a',1],['b',2]]
425        ```
426
427        ```
428        def myFunction(a,b,c):
429            return [a,b,c]
430
431        curriedMyFn=pamda.curry(myFunction)
432
433        curriedMyFn(1,2,3) #=> [1,2,3]
434        curriedMyFn(1)(2,3) #=> [1,2,3]
435
436        x=curriedMyFn(1)(2)
437        x(3) #=> [1,2,3]
438        x(4) #=> [1,2,4]
439
440
441        ```
442        """
443        if fn.__dict__.get("__isCurried__"):
444            return fn()
445        return curry_obj(fn)

Function:

  • Curries a function such that inputs can be added interatively

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to curry
    • Note: Class methods auto apply self during curry

Notes:

  • Once curried, the function | method becomes a curry_obj object
  • The initial function is only called once all inputs are passed

Examples:

curriedZip=pamda.curry(pamda.zip)
curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]]

# Curried functions can be thunkified at any time
# See also thunkify
zipThunk=curriedZip.thunkify()(['a','b'])([1,2])
zipThunk() #=> [['a',1],['b',2]]
def myFunction(a,b,c):
    return [a,b,c]

curriedMyFn=pamda.curry(myFunction)

curriedMyFn(1,2,3) #=> [1,2,3]
curriedMyFn(1)(2,3) #=> [1,2,3]

x=curriedMyFn(1)(2)
x(3) #=> [1,2,3]
x(4) #=> [1,2,4]


def curryTyped(self, fn):
447    def curryTyped(self, fn):
448        """
449        Function:
450
451        - Curries a function such that inputs can be added interatively and function annotations are type checked at runtime
452
453        Requires:
454
455        - `fn`:
456            - Type: function | method
457            - What: The function or method to curry
458            - Note: Class methods auto apply self during curry
459
460        Notes:
461
462        - Once curried, the function | method becomes a curry_obj object
463        - The initial function is only called once all inputs are passed
464
465
466        Examples:
467
468        ```
469        @pamda.curryTyped
470        def add(a:int,b:int):
471            return a+b
472
473        add(1)(1) #=> 2
474        add(1)(1.5) #=> Raises type exception
475        ```
476        """
477        if fn.__dict__.get("__isCurried__"):
478            return fn().typeEnforce()
479        return curry_obj(fn).typeEnforce()

Function:

  • Curries a function such that inputs can be added interatively and function annotations are type checked at runtime

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to curry
    • Note: Class methods auto apply self during curry

Notes:

  • Once curried, the function | method becomes a curry_obj object
  • The initial function is only called once all inputs are passed

Examples:

@pamda.curryTyped
def add(a:int,b:int):
    return a+b

add(1)(1) #=> 2
add(1)(1.5) #=> Raises type exception
def dec(self, a: int | float):
481    def dec(self, a: int | float):
482        """
483        Function:
484
485        - Decrements a number by one
486
487        Requires:
488
489        - `a`:
490            - Type: int | float
491            - What: The number to decrement
492
493        Example:
494
495        ```
496        pamda.dec(42) #=> 41
497        ```
498        """
499        if not isinstance(a, (int, float)):
500            raise Exception("`a` must be an `int` or a `float`")
501        return a - 1

Function:

  • Decrements a number by one

Requires:

  • a:
    • Type: int | float
    • What: The number to decrement

Example:

pamda.dec(42) #=> 41
def difference(self, a: list, b: list):
503    def difference(self, a: list, b: list):
504        """
505        Function:
506
507        - Combines two lists into a list of no duplicate items present in the first list but not the second
508
509        Requires:
510
511        - `a`:
512            - Type: list
513            - What: List of items in which to look for a difference
514        - `b`:
515            - Type: list
516            - What: List of items in which to compare when looking for the difference
517
518        Example:
519
520        ```
521        a=['a','b']
522        b=['b','c']
523        pamda.difference(a=a, b=b) #=> ['a']
524        pamda.difference(a=b, b=a) #=> ['c']
525        ```
526        """
527        return list(set(a).difference(set(b)))

Function:

  • Combines two lists into a list of no duplicate items present in the first list but not the second

Requires:

  • a:
    • Type: list
    • What: List of items in which to look for a difference
  • b:
    • Type: list
    • What: List of items in which to compare when looking for the difference

Example:

a=['a','b']
b=['b','c']
pamda.difference(a=a, b=b) #=> ['a']
pamda.difference(a=b, b=a) #=> ['c']
def dissocPath(self, path: list | str | int | tuple, data: dict):
529    def dissocPath(self, path: list | str | int | tuple, data: dict):
530        """
531        Function:
532
533        - Removes the value at the end of a path within a nested dictionary
534        - Note: This updates the object in place, but also returns the object
535
536        Requires:
537
538        - `path`:
539            - Type: list of strs | str
540            - What: The path to remove from the dictionary
541            - Note: If a string is passed, assumes a single item path list with that string
542        - `data`:
543            - Type: dict
544            - What: A dictionary with a path to be removed
545
546        Example:
547
548        ```
549        data={'a':{'b':{'c':0,'d':1}}}
550        pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}}
551        ```
552        """
553        if not isinstance(path, list):
554            path = [path]
555        if not self.hasPath(path=path, data=data):
556            raise Exception("Path does not exist")
557        else:
558            reduce(__getForceDict__, path[:-1], data).pop(path[-1])
559        return data

Function:

  • Removes the value at the end of a path within a nested dictionary
  • Note: This updates the object in place, but also returns the object

Requires:

  • path:
    • Type: list of strs | str
    • What: The path to remove from the dictionary
    • Note: If a string is passed, assumes a single item path list with that string
  • data:
    • Type: dict
    • What: A dictionary with a path to be removed

Example:

data={'a':{'b':{'c':0,'d':1}}}
pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}}
def flatten(self, data: list):
561    def flatten(self, data: list):
562        """
563        Function:
564
565        - Flattens a list of lists of lists ... into a single list depth first
566
567        Requires:
568
569        - `data`:
570            - Type: list of lists
571            - What: The list of lists to reduce to a single list
572        Example:
573
574        ```
575        data=[['a','b'],[1,[2]]]
576        pamda.flatten(data=data) #=> ['a','b',1,2]
577        ```
578        """
579
580        def iter_flatten(data):
581            out = []
582            for i in data:
583                if isinstance(i, list):
584                    out.extend(iter_flatten(i))
585                else:
586                    out.append(i)
587            return out
588
589        return iter_flatten(data)

Function:

  • Flattens a list of lists of lists ... into a single list depth first

Requires:

  • data:
    • Type: list of lists
    • What: The list of lists to reduce to a single list Example:
data=[['a','b'],[1,[2]]]
pamda.flatten(data=data) #=> ['a','b',1,2]
def flip(self, fn):
591    def flip(self, fn):
592        """
593        Function:
594
595        - Returns a new function equivalent to the supplied function except that the first two inputs are flipped
596
597        Requires:
598
599        - `fn`:
600            - Type: function | method
601            - What: The function or method to flip
602            - Note: This function must have an arity of at least 2 (take two inputs)
603            - Note: Only args are flipped, kwargs are passed as normal
604
605        Notes:
606
607        - Input functions are not flipped in place
608        - The returned function is a flipped version of the input function
609        - A curried function can be flipped in place by calling fn.flip()
610        - A function can be flipped multiple times:
611            - At each flip, the first and second inputs for the function as it is currently curried are switched
612            - Flipping a function two times before adding an input will return the initial value
613
614        Examples:
615
616        ```
617        def concat(a,b,c,d):
618            return str(a)+str(b)+str(c)+str(d)
619
620        flip_concat=pamda.flip(concat)
621
622        concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
623        flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
624        ```
625
626        ```
627        @pamda.curry
628        def concat(a,b,c,d):
629            return str(a)+str(b)+str(c)+str(d)
630
631        concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
632
633        concat.flip()
634
635        concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
636        ```
637
638        ```
639        @pamda.curry
640        def concat(a,b,c,d):
641            return str(a)+str(b)+str(c)+str(d)
642
643        a=pamda.flip(concat)('fi-')
644        b=pamda.flip(a)('fo-')
645        c=pamda.flip(b)('fum')
646        c('fe-') #=> 'fe-fi-fo-fum'
647        ```
648
649        ```
650        def concat(a,b,c,d):
651            return str(a)+str(b)+str(c)+str(d)
652
653        a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum')
654        a('fe-') #=> 'fe-fi-fo-fum'
655        ```
656        """
657        fn = self.curry(fn)
658        return fn.flip()

Function:

  • Returns a new function equivalent to the supplied function except that the first two inputs are flipped

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to flip
    • Note: This function must have an arity of at least 2 (take two inputs)
    • Note: Only args are flipped, kwargs are passed as normal

Notes:

  • Input functions are not flipped in place
  • The returned function is a flipped version of the input function
  • A curried function can be flipped in place by calling fn.flip()
  • A function can be flipped multiple times:
    • At each flip, the first and second inputs for the function as it is currently curried are switched
    • Flipping a function two times before adding an input will return the initial value

Examples:

def concat(a,b,c,d):
    return str(a)+str(b)+str(c)+str(d)

flip_concat=pamda.flip(concat)

concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
@pamda.curry
def concat(a,b,c,d):
    return str(a)+str(b)+str(c)+str(d)

concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'

concat.flip()

concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
@pamda.curry
def concat(a,b,c,d):
    return str(a)+str(b)+str(c)+str(d)

a=pamda.flip(concat)('fi-')
b=pamda.flip(a)('fo-')
c=pamda.flip(b)('fum')
c('fe-') #=> 'fe-fi-fo-fum'
def concat(a,b,c,d):
    return str(a)+str(b)+str(c)+str(d)

a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum')
a('fe-') #=> 'fe-fi-fo-fum'
def getArity(self, fn):
660    def getArity(self, fn):
661        """
662        Function:
663
664        - Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried)
665
666        Requires:
667
668        - `fn`:
669            - Type: function | method
670            - What: The function or method to get the arity of
671            - Note: Class methods remove one arity to account for self
672
673        Examples:
674
675        ```
676        pamda.getArity(pamda.zip) #=> 2
677        curriedZip=pamda.curry(pamda.zip)
678        ABCuriedZip=curriedZip(['a','b'])
679        pamda.getArity(ABCuriedZip) #=> 1
680        ```
681        """
682        fn = self.curry(fn)
683        return fn.__arity__

Function:

  • Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried)

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to get the arity of
    • Note: Class methods remove one arity to account for self

Examples:

pamda.getArity(pamda.zip) #=> 2
curriedZip=pamda.curry(pamda.zip)
ABCuriedZip=curriedZip(['a','b'])
pamda.getArity(ABCuriedZip) #=> 1
def groupBy(self, fn, data: list):
685    def groupBy(self, fn, data: list):
686        """
687        Function:
688
689        - Splits a list into a dictionary of sublists keyed by the return string of a provided function
690
691        Requires:
692
693        - `fn`:
694            - Type: function | method
695            - What: The function or method to group by
696            - Note: Must return a string (or other hashable object)
697            - Note: This function must be unary (take one input)
698            - Note: This function is applied to each item in the list recursively
699        - `data`:
700            - Type: list
701            - What: List of items to apply the function to and then group by the results
702
703        Examples:
704
705        ```
706        def getGrade(item):
707            score=item['score']
708            if score>90:
709                return 'A'
710            elif score>80:
711                return 'B'
712            elif score>70:
713                return 'C'
714            elif score>60:
715                return 'D'
716            else:
717                return 'F'
718
719        data=[
720            {'name':'Connor', 'score':75},
721            {'name':'Fred', 'score':79},
722            {'name':'Joe', 'score':84},
723        ]
724        pamda.groupBy(getGrade,data)
725        #=>{
726        #=>    'B':[{'name':'Joe', 'score':84}]
727        #=>    'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}]
728        #=>}
729        ```
730        """
731        curried_fn = self.curry(fn)
732        if curried_fn.__arity__ != 1:
733            raise Exception(
734                "groupBy `fn` must only take one parameter as its input"
735            )
736        return __groupByHashable__(fn=fn, data=data)

Function:

  • Splits a list into a dictionary of sublists keyed by the return string of a provided function

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to group by
    • Note: Must return a string (or other hashable object)
    • Note: This function must be unary (take one input)
    • Note: This function is applied to each item in the list recursively
  • data:
    • Type: list
    • What: List of items to apply the function to and then group by the results

Examples:

def getGrade(item):
    score=item['score']
    if score>90:
        return 'A'
    elif score>80:
        return 'B'
    elif score>70:
        return 'C'
    elif score>60:
        return 'D'
    else:
        return 'F'

data=[
    {'name':'Connor', 'score':75},
    {'name':'Fred', 'score':79},
    {'name':'Joe', 'score':84},
]
pamda.groupBy(getGrade,data)
#=>{
#=>    'B':[{'name':'Joe', 'score':84}]
#=>    'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}]
#=>}
def groupKeys(self, keys: list, data: list):
738    def groupKeys(self, keys: list, data: list):
739        """
740        Function:
741
742        - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys
743
744        Requires:
745
746        - `keys`:
747            - Type: list of strs
748            - What: The keys to group by
749        - `data`:
750            - Type: list of dicts
751            - What: List of dictionaries with which to match keys
752
753        Examples:
754
755        ```
756        data=[
757            {'color':'red', 'size':9, 'shape':'ball'},
758            {'color':'red', 'size':10, 'shape':'ball'},
759            {'color':'green', 'size':11, 'shape':'ball'},
760            {'color':'green', 'size':12, 'shape':'square'}
761        ]
762        pamda.groupKeys(['color','shape'],data)
763        #=> [
764        #=>     [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}],
765        #=>     [{'color': 'green', 'size': 11, 'shape': 'ball'}],
766        #=>     [{'color': 'green', 'size': 12, 'shape': 'square'}]
767        #=> ]
768        ```
769        """
770
771        def key_fn(item):
772            return tuple([item[key] for key in keys])
773
774        return list(__groupByHashable__(key_fn, data).values())

Function:

  • Splits a list of dicts into a list of sublists of dicts separated by values with equal keys

Requires:

  • keys:
    • Type: list of strs
    • What: The keys to group by
  • data:
    • Type: list of dicts
    • What: List of dictionaries with which to match keys

Examples:

data=[
    {'color':'red', 'size':9, 'shape':'ball'},
    {'color':'red', 'size':10, 'shape':'ball'},
    {'color':'green', 'size':11, 'shape':'ball'},
    {'color':'green', 'size':12, 'shape':'square'}
]
pamda.groupKeys(['color','shape'],data)
#=> [
#=>     [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}],
#=>     [{'color': 'green', 'size': 11, 'shape': 'ball'}],
#=>     [{'color': 'green', 'size': 12, 'shape': 'square'}]
#=> ]
def groupWith(self, fn, data: list):
776    def groupWith(self, fn, data: list):
777        """
778        Function:
779
780        - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function
781
782        Requires:
783
784        - `fn`:
785            - Type: function | method
786            - What: The function or method to groub with
787            - Note: Must return a boolean value
788            - Note: This function must have an arity of two (take two inputs)
789            - Note: This function is applied to each item plus the next adjacent item in the list recursively
790        - `data`:
791            - Type: list
792            - What: List of items to apply the function to and then group the results
793
794        Examples:
795
796        ```
797        def areEqual(a,b):
798            return a==b
799
800        data=[1,2,3,1,1,2,2,3,3,3]
801        pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]]
802        ```
803        """
804        curried_fn = self.curry(fn)
805        if curried_fn.__arity__ != 2:
806            raise Exception("groupWith `fn` must take exactly two parameters")
807        previous = data[0]
808        output = []
809        sublist = [previous]
810        for i in data[1:]:
811            if fn(i, previous):
812                sublist.append(i)
813            else:
814                output.append(sublist)
815                sublist = [i]
816            previous = i
817        output.append(sublist)
818        return output

Function:

  • Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to groub with
    • Note: Must return a boolean value
    • Note: This function must have an arity of two (take two inputs)
    • Note: This function is applied to each item plus the next adjacent item in the list recursively
  • data:
    • Type: list
    • What: List of items to apply the function to and then group the results

Examples:

def areEqual(a,b):
    return a==b

data=[1,2,3,1,1,2,2,3,3,3]
pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]]
def hasPath(self, path: list | str, data: dict):
820    def hasPath(self, path: list | str, data: dict):
821        """
822        Function:
823
824        - Checks if a path exists within a nested dictionary
825
826        Requires:
827
828        - `path`:
829            - Type: list of strs | str
830            - What: The path to check
831            - Note: If a string is passed, assumes a single item path list with that string
832        - `data`:
833            - Type: dict
834            - What: A dictionary to check if the path exists
835
836        Example:
837
838        ```
839        data={'a':{'b':1}}
840        pamda.hasPath(path=['a','b'], data=data) #=> True
841        pamda.hasPath(path=['a','d'], data=data) #=> False
842        ```
843        """
844        if isinstance(path, str):
845            path = [path]
846        try:
847            reduce(lambda x, y: x[y], path, data)
848            return True
849        except (KeyError, IndexError, TypeError):
850            return False

Function:

  • Checks if a path exists within a nested dictionary

Requires:

  • path:
    • Type: list of strs | str
    • What: The path to check
    • Note: If a string is passed, assumes a single item path list with that string
  • data:
    • Type: dict
    • What: A dictionary to check if the path exists

Example:

data={'a':{'b':1}}
pamda.hasPath(path=['a','b'], data=data) #=> True
pamda.hasPath(path=['a','d'], data=data) #=> False
def hardRound(self, decimal_places: int, a: int | float):
852    def hardRound(self, decimal_places: int, a: int | float):
853        """
854        Function:
855
856        - Rounds to a set number of decimal places regardless of floating point math in python
857
858        Requires:
859
860        - `decimal_places`:
861            - Type: int
862            - What: The number of decimal places to round to
863            - Default: 0
864            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
865        - `a`:
866            - Type: int | float
867            - What: The number to round
868
869        Example:
870
871        ```
872        a=12.345
873        pamda.hardRound(1,a) #=> 12.3
874        pamda.hardRound(-1,a) #=> 10
875        ```
876        """
877        return int(a * (10**decimal_places) + 0.5) / (10**decimal_places)

Function:

  • Rounds to a set number of decimal places regardless of floating point math in python

Requires:

  • decimal_places:
    • Type: int
    • What: The number of decimal places to round to
    • Default: 0
    • Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
  • a:
    • Type: int | float
    • What: The number to round

Example:

a=12.345
pamda.hardRound(1,a) #=> 12.3
pamda.hardRound(-1,a) #=> 10
def head(self, data: list | str):
879    def head(self, data: list | str):
880        """
881        Function:
882
883        - Picks the first item out of a list or string
884
885        Requires:
886
887        - `data`:
888            - Type: list | str
889            - What: A list or string
890
891        Example:
892
893        ```
894        data=['fe','fi','fo','fum']
895        pamda.first(
896            data=data
897        ) #=> fe
898        ```
899        """
900        if not isinstance(data, (list, str)):
901            raise Exception("`head` can only be called on a `str` or a `list`")
902        if not len(data) > 0:
903            raise Exception("Attempting to call `head` on an empty list or str")
904        return data[0]

Function:

  • Picks the first item out of a list or string

Requires:

  • data:
    • Type: list | str
    • What: A list or string

Example:

data=['fe','fi','fo','fum']
pamda.first(
    data=data
) #=> fe
def inc(self, a: int | float):
906    def inc(self, a: int | float):
907        """
908        Function:
909
910        - Increments a number by one
911
912        Requires:
913
914        - `a`:
915            - Type: int | float
916            - What: The number to increment
917
918        Example:
919
920        ```
921        pamda.inc(42) #=> 43
922        ```
923        """
924        if not isinstance(a, (int, float)):
925            raise Exception("`a` must be an `int` or a `float`")
926        return a + 1

Function:

  • Increments a number by one

Requires:

  • a:
    • Type: int | float
    • What: The number to increment

Example:

pamda.inc(42) #=> 43
def intersection(self, a: list, b: list):
928    def intersection(self, a: list, b: list):
929        """
930        Function:
931
932        - Combines two lists into a list of no duplicates composed of those elements common to both lists
933
934        Requires:
935
936        - `a`:
937            - Type: list
938            - What: List of items in which to look for an intersection
939        - `b`:
940            - Type: list
941            - What: List of items in which to look for an intersection
942
943        Example:
944
945        ```
946        a=['a','b']
947        b=['b','c']
948        pamda.intersection(a=a, b=b) #=> ['b']
949        ```
950        """
951        return list(set(a).intersection(set(b)))

Function:

  • Combines two lists into a list of no duplicates composed of those elements common to both lists

Requires:

  • a:
    • Type: list
    • What: List of items in which to look for an intersection
  • b:
    • Type: list
    • What: List of items in which to look for an intersection

Example:

a=['a','b']
b=['b','c']
pamda.intersection(a=a, b=b) #=> ['b']
def map(self, fn, data: list | dict):
 953    def map(self, fn, data: list | dict):
 954        """
 955        Function:
 956
 957        - Maps a function over a list or a dictionary
 958
 959        Requires:
 960
 961        - `fn`:
 962            - Type: function | method
 963            - What: The function or method to map over the list or dictionary
 964            - Note: This function should have an arity of 1
 965        - `data`:
 966            - Type: list | dict
 967            - What: The list or dict of items to map the function over
 968
 969        Examples:
 970
 971        ```
 972        data=[1,2,3]
 973        pamda.map(
 974            fn=pamda.inc,
 975            data=data
 976        )
 977        #=> [2,3,4]
 978        ```
 979
 980        ```
 981        data={'a':1,'b':2,'c':3}
 982        pamda.map(
 983            fn=pamda.inc,
 984            data=data
 985        )
 986        #=> {'a':2,'b':3,'c':4}
 987        ```
 988
 989        """
 990        # TODO: Check for efficiency gains
 991        fn = self.curry(fn)
 992        if fn.__arity__ != 1:
 993            raise Exception("`map` `fn` must be unary (take one input)")
 994        if not len(data) > 0:
 995            raise Exception(
 996                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
 997            )
 998        if isinstance(data, dict):
 999            return {key: fn(value) for key, value in data.items()}
1000        else:
1001            return [fn(i) for i in data]

Function:

  • Maps a function over a list or a dictionary

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to map over the list or dictionary
    • Note: This function should have an arity of 1
  • data:
    • Type: list | dict
    • What: The list or dict of items to map the function over

Examples:

data=[1,2,3]
pamda.map(
    fn=pamda.inc,
    data=data
)
#=> [2,3,4]
data={'a':1,'b':2,'c':3}
pamda.map(
    fn=pamda.inc,
    data=data
)
#=> {'a':2,'b':3,'c':4}
def mean(self, data: list):
1003    def mean(self, data: list):
1004        """
1005        Function:
1006
1007        - Calculates the mean of a given list
1008
1009        Requires:
1010
1011        - `data`:
1012            - Type: list of (floats | ints)
1013            - What: The list with wich to calculate the mean
1014            - Note: If the length of this list is 0, returns None
1015
1016        Example:
1017
1018        ```
1019        data=[1,2,3]
1020        pamda.mean(data=data)
1021        #=> 2
1022        ```
1023
1024        ```
1025        data=[]
1026        pamda.mean(data=data)
1027        #=> None
1028        ```
1029        """
1030        if len(data) == 0:
1031            return None
1032        return sum(data) / len(data)

Function:

  • Calculates the mean of a given list

Requires:

  • data:
    • Type: list of (floats | ints)
    • What: The list with wich to calculate the mean
    • Note: If the length of this list is 0, returns None

Example:

data=[1,2,3]
pamda.mean(data=data)
#=> 2
data=[]
pamda.mean(data=data)
#=> None
def median(self, data: list):
1034    def median(self, data: list):
1035        """
1036        Function:
1037
1038        - Calculates the median of a given list
1039        - If the length of the list is even, calculates the mean of the two central values
1040
1041        Requires:
1042
1043        - `data`:
1044            - Type: list of (floats | ints)
1045            - What: The list with wich to calculate the mean
1046            - Note: If the length of this list is 0, returns None
1047
1048        Examples:
1049
1050        ```
1051        data=[7,2,8,9]
1052        pamda.median(data=data)
1053        #=> 7.5
1054        ```
1055
1056        ```
1057        data=[7,8,9]
1058        pamda.median(data=data)
1059        #=> 8
1060        ```
1061
1062        ```
1063        data=[]
1064        pamda.median(data=data)
1065        #=> None
1066        ```
1067        """
1068        if not isinstance(data, (list)):
1069            raise Exception("`median` `data` must be a list")
1070        length = len(data)
1071        if length == 0:
1072            return None
1073        data = sorted(data)
1074        if length % 2 == 0:
1075            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1076        return data[int(length / 2)]

Function:

  • Calculates the median of a given list
  • If the length of the list is even, calculates the mean of the two central values

Requires:

  • data:
    • Type: list of (floats | ints)
    • What: The list with wich to calculate the mean
    • Note: If the length of this list is 0, returns None

Examples:

data=[7,2,8,9]
pamda.median(data=data)
#=> 7.5
data=[7,8,9]
pamda.median(data=data)
#=> 8
data=[]
pamda.median(data=data)
#=> None
def mergeDeep(self, update_data, data):
1078    def mergeDeep(self, update_data, data):
1079        """
1080        Function:
1081
1082        - Recursively merges two nested dictionaries keeping all keys at each layer
1083        - Values from `update_data` are used when keys are present in both dictionaries
1084
1085        Requires:
1086
1087        - `update_data`:
1088            - Type: any
1089            - What: The new data that will take precedence during merging
1090        - `data`:
1091            - Type: any
1092            - What: The original data that will be merged into
1093
1094        Example:
1095
1096        ```
1097        data={'a':{'b':{'c':'d'},'e':'f'}}
1098        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1099        pamda.mergeDeep(
1100            update_data=update_data,
1101            data=data
1102        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1103        ```
1104        """
1105        return __mergeDeep__(update_data, data)

Function:

  • Recursively merges two nested dictionaries keeping all keys at each layer
  • Values from update_data are used when keys are present in both dictionaries

Requires:

  • update_data:
    • Type: any
    • What: The new data that will take precedence during merging
  • data:
    • Type: any
    • What: The original data that will be merged into

Example:

data={'a':{'b':{'c':'d'},'e':'f'}}
update_data={'a':{'b':{'h':'i'},'e':'g'}}
pamda.mergeDeep(
    update_data=update_data,
    data=data
) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
def nest(self, path_keys: list, value_key: str, data: list):
1107    def nest(self, path_keys: list, value_key: str, data: list):
1108        """
1109        Function:
1110
1111        - Nests a list of dictionaries into a nested dictionary
1112        - Similar items are appended to a list in the end of the nested dictionary
1113
1114        Requires:
1115
1116        - `path_keys`:
1117            - Type: list of strs
1118            - What: The variables to pull from each item in data
1119            - Note: Used to build out the nested dicitonary
1120            - Note: Order matters as the nesting occurs in order of variable
1121        - `value_key`:
1122            - Type: str
1123            - What: The variable to add to the list at the end of the nested dictionary path
1124        - `data`:
1125            - Type: list of dicts
1126            - What: A list of dictionaries to use for nesting purposes
1127
1128        Example:
1129
1130        ```
1131        data=[
1132            {'x_1':'a','x_2':'b', 'output':'c'},
1133            {'x_1':'a','x_2':'b', 'output':'d'},
1134            {'x_1':'a','x_2':'e', 'output':'f'}
1135        ]
1136        pamda.nest(
1137            path_keys=['x_1','x_2'],
1138            value_key='output',
1139            data=data
1140        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1141        ```
1142        """
1143        if not isinstance(data, list):
1144            raise Exception("Attempting to `nest` an object that is not a list")
1145        if len(data) == 0:
1146            raise Exception("Attempting to `nest` from an empty list")
1147        nested_output = {}
1148        for item in self.groupKeys(keys=path_keys, data=data):
1149            nested_output = __assocPath__(
1150                path=__getKeyValues__(path_keys, item[0]),
1151                value=[i.get(value_key) for i in item],
1152                data=nested_output,
1153            )
1154        return nested_output

Function:

  • Nests a list of dictionaries into a nested dictionary
  • Similar items are appended to a list in the end of the nested dictionary

Requires:

  • path_keys:
    • Type: list of strs
    • What: The variables to pull from each item in data
    • Note: Used to build out the nested dicitonary
    • Note: Order matters as the nesting occurs in order of variable
  • value_key:
    • Type: str
    • What: The variable to add to the list at the end of the nested dictionary path
  • data:
    • Type: list of dicts
    • What: A list of dictionaries to use for nesting purposes

Example:

data=[
    {'x_1':'a','x_2':'b', 'output':'c'},
    {'x_1':'a','x_2':'b', 'output':'d'},
    {'x_1':'a','x_2':'e', 'output':'f'}
]
pamda.nest(
    path_keys=['x_1','x_2'],
    value_key='output',
    data=data
) #=> {'a':{'b':['c','d'], 'e':['f']}}
def nestItem(self, path_keys: list, data: list):
1156    def nestItem(self, path_keys: list, data: list):
1157        """
1158        Function:
1159
1160        - Nests a list of dictionaries into a nested dictionary
1161        - Similar items are appended to a list in the end of the nested dictionary
1162        - Similar to `nest`, except no values are plucked for the aggregated list
1163
1164        Requires:
1165
1166        - `path_keys`:
1167            - Type: list of strs
1168            - What: The variables to pull from each item in data
1169            - Note: Used to build out the nested dicitonary
1170            - Note: Order matters as the nesting occurs in order of variable
1171        - `data`:
1172            - Type: list of dicts
1173            - What: A list of dictionaries to use for nesting purposes
1174
1175        Example:
1176
1177        ```
1178        data=[
1179            {'x_1':'a','x_2':'b'},
1180            {'x_1':'a','x_2':'b'},
1181            {'x_1':'a','x_2':'e'}
1182        ]
1183        pamda.nestItem
1184            path_keys=['x_1','x_2'],
1185            data=data
1186        )
1187        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1188
1189        ```
1190        """
1191        if not isinstance(data, list):
1192            raise Exception("Attempting to `nest` an object that is not a list")
1193        if len(data) == 0:
1194            raise Exception("Attempting to `nest` from an empty list")
1195        nested_output = {}
1196        for item in self.groupKeys(keys=path_keys, data=data):
1197            nested_output = __assocPath__(
1198                path=__getKeyValues__(path_keys, item[0]),
1199                value=item,
1200                data=nested_output,
1201            )
1202        return nested_output

Function:

  • Nests a list of dictionaries into a nested dictionary
  • Similar items are appended to a list in the end of the nested dictionary
  • Similar to nest, except no values are plucked for the aggregated list

Requires:

  • path_keys:
    • Type: list of strs
    • What: The variables to pull from each item in data
    • Note: Used to build out the nested dicitonary
    • Note: Order matters as the nesting occurs in order of variable
  • data:
    • Type: list of dicts
    • What: A list of dictionaries to use for nesting purposes

Example:

data=[
    {'x_1':'a','x_2':'b'},
    {'x_1':'a','x_2':'b'},
    {'x_1':'a','x_2':'e'}
]
pamda.nestItem
    path_keys=['x_1','x_2'],
    data=data
)
#=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}

def path(self, path: list | str, data: dict):
1204    def path(self, path: list | str, data: dict):
1205        """
1206        Function:
1207
1208        - Returns the value of a path within a nested dictionary or None if the path does not exist
1209
1210        Requires:
1211
1212        - `path`:
1213            - Type: list of strs | str
1214            - What: The path to pull given the data
1215            - Note: If a string is passed, assumes a single item path list with that string
1216        - `data`:
1217            - Type: dict
1218            - What: A dictionary to get the path from
1219
1220        Example:
1221
1222        ```
1223        data={'a':{'b':1}}
1224        pamda.path(path=['a','b'], data=data) #=> 1
1225        ```
1226        """
1227        if isinstance(path, str):
1228            path = [path]
1229        return __pathOr__(None, path, data)

Function:

  • Returns the value of a path within a nested dictionary or None if the path does not exist

Requires:

  • path:
    • Type: list of strs | str
    • What: The path to pull given the data
    • Note: If a string is passed, assumes a single item path list with that string
  • data:
    • Type: dict
    • What: A dictionary to get the path from

Example:

data={'a':{'b':1}}
pamda.path(path=['a','b'], data=data) #=> 1
def pathOr(self, default, path: list | str, data: dict):
1231    def pathOr(self, default, path: list | str, data: dict):
1232        """
1233        Function:
1234
1235        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1236
1237        Requires:
1238
1239        - `default`:
1240            - Type: any
1241            - What: The object to return if the path does not exist
1242        - `path`:
1243            - Type: list of strs | str
1244            - What: The path to pull given the data
1245            - Note: If a string is passed, assumes a single item path list with that string
1246        - `data`:
1247            - Type: dict
1248            - What: A dictionary to get the path from
1249
1250        Example:
1251
1252        ```
1253        data={'a':{'b':1}}
1254        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1255        ```
1256        """
1257        if isinstance(path, str):
1258            path = [path]
1259        try:
1260            return reduce(lambda x, y: x[y], path, data)
1261        except (KeyError, IndexError, TypeError):
1262            return default

Function:

  • Returns the value of a path within a nested dictionary or a default value if that path does not exist

Requires:

  • default:
    • Type: any
    • What: The object to return if the path does not exist
  • path:
    • Type: list of strs | str
    • What: The path to pull given the data
    • Note: If a string is passed, assumes a single item path list with that string
  • data:
    • Type: dict
    • What: A dictionary to get the path from

Example:

data={'a':{'b':1}}
pamda.path(default=2, path=['a','c'], data=data) #=> 2
def pipe(self, fns: list, args: tuple, kwargs: dict):
1264    def pipe(self, fns: list, args: tuple, kwargs: dict):
1265        """
1266        Function:
1267
1268        - Pipes data through n functions in order (left to right composition) and returns the output
1269
1270        Requires:
1271
1272        - `fns`:
1273            - Type: list of (functions | methods)
1274            - What: The list of functions and methods to pipe the data through
1275            - Notes: The first function in the list can be any arity (accepting any number of inputs)
1276            - Notes: Any further function in the list can only be unary (single input)
1277            - Notes: A function can be curried, but is not required to be
1278            - Notes: You may opt to curry functions and add inputs to make them unary
1279        - `args`:
1280            - Type: tuple
1281            - What: a tuple of positional arguments to pass to the first function in `fns`
1282        - `kwargs`:
1283            - Type: dict
1284            - What: a dictionary of keyword arguments to pass to the first function in `fns`
1285
1286        Examples:
1287
1288        ```
1289        data=['abc','def']
1290        pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
1291        pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
1292        ```
1293
1294        ```
1295        data={'a':{'b':'c'}}
1296        curriedPath=pamda.curry(pamda.path)
1297        pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1298        ```
1299        """
1300        if len(fns) == 0:
1301            raise Exception("`fns` must be a list with at least one function")
1302        if self.getArity(fns[0]) == 0:
1303            raise Exception(
1304                "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0."
1305            )
1306        if not all([(self.getArity(fn) == 1) for fn in fns[1:]]):
1307            raise Exception(
1308                "Only the first function in `fns` can have n arity (accept n args). All other functions must have an arity of one (accepting one argument)."
1309            )
1310        out = fns[0](*args, **kwargs)
1311        for fn in fns[1:]:
1312            out = fn(out)
1313        return out

Function:

  • Pipes data through n functions in order (left to right composition) and returns the output

Requires:

  • fns:
    • Type: list of (functions | methods)
    • What: The list of functions and methods to pipe the data through
    • Notes: The first function in the list can be any arity (accepting any number of inputs)
    • Notes: Any further function in the list can only be unary (single input)
    • Notes: A function can be curried, but is not required to be
    • Notes: You may opt to curry functions and add inputs to make them unary
  • args:
    • Type: tuple
    • What: a tuple of positional arguments to pass to the first function in fns
  • kwargs:
    • Type: dict
    • What: a dictionary of keyword arguments to pass to the first function in fns

Examples:

data=['abc','def']
pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
data={'a':{'b':'c'}}
curriedPath=pamda.curry(pamda.path)
pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
def pivot(self, data: list[dict] | dict[typing.Any, list]):
1315    def pivot(self, data: list[dict] | dict[Any, list]):
1316        """
1317        Function:
1318
1319        - Pivots a list of dictionaries into a dictionary of lists
1320        - Pivots a dictionary of lists into a list of dictionaries
1321
1322        Requires:
1323
1324        - `data`:
1325            - Type: list of dicts | dict of lists
1326            - What: The data to pivot
1327            - Note: If a list of dictionaries is passed, all dictionaries must have the same keys
1328            - Note: If a dictionary of lists is passed, all lists must have the same length
1329
1330        Example:
1331
1332        ```
1333        data=[
1334            {'a':1,'b':2},
1335            {'a':3,'b':4}
1336        ]
1337        pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
1338
1339        data={'a':[1,3],'b':[2,4]}
1340        pamda.pivot(data=data)
1341        #=> [
1342        #=>     {'a':1,'b':2},
1343        #=>     {'a':3,'b':4}
1344        #=> ]
1345        ```
1346        """
1347        if isinstance(data, list):
1348            return {
1349                key: [record[key] for record in data] for key in data[0].keys()
1350            }
1351        else:
1352            return [
1353                {key: data[key][i] for key in data.keys()}
1354                for i in range(len(data[list(data.keys())[0]]))
1355            ]

Function:

  • Pivots a list of dictionaries into a dictionary of lists
  • Pivots a dictionary of lists into a list of dictionaries

Requires:

  • data:
    • Type: list of dicts | dict of lists
    • What: The data to pivot
    • Note: If a list of dictionaries is passed, all dictionaries must have the same keys
    • Note: If a dictionary of lists is passed, all lists must have the same length

Example:

data=[
    {'a':1,'b':2},
    {'a':3,'b':4}
]
pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}

data={'a':[1,3],'b':[2,4]}
pamda.pivot(data=data)
#=> [
#=>     {'a':1,'b':2},
#=>     {'a':3,'b':4}
#=> ]
def pluck(self, path: list | str, data: list):
1357    def pluck(self, path: list | str, data: list):
1358        """
1359        Function:
1360
1361        - Returns the values of a path within a list of nested dictionaries
1362
1363        Requires:
1364
1365        - `path`:
1366            - Type: list of strs
1367            - What: The path to pull given the data
1368            - Note: If a string is passed, assumes a single item path list with that string
1369        - `data`:
1370            - Type: list of dicts
1371            - What: A list of dictionaries to get the path from
1372
1373        Example:
1374
1375        ```
1376        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1377        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1378        ```
1379        """
1380        if len(data) == 0:
1381            raise Exception("Attempting to pluck from an empty list")
1382        if isinstance(path, str):
1383            path = [path]
1384        return [__pathOr__(default=None, path=path, data=i) for i in data]

Function:

  • Returns the values of a path within a list of nested dictionaries

Requires:

  • path:
    • Type: list of strs
    • What: The path to pull given the data
    • Note: If a string is passed, assumes a single item path list with that string
  • data:
    • Type: list of dicts
    • What: A list of dictionaries to get the path from

Example:

data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
pamda.pluck(path=['a','b'], data=data) #=> [1,2]
def pluckIf(self, fn, path: list | str, data: list):
1386    def pluckIf(self, fn, path: list | str, data: list):
1387        """
1388        Function:
1389
1390        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1391
1392        Requires:
1393
1394        - `fn`:
1395            - Type: function
1396            - What: A function to take in each item in data and return a boolean
1397            - Note: Only items that return true are plucked
1398            - Note: Should be a unary function (take one input)
1399        - `path`:
1400            - Type: list of strs
1401            - What: The path to pull given the data
1402            - Note: If a string is passed, assumes a single item path list with that string
1403        - `data`:
1404            - Type: list of dicts
1405            - What: A list of dictionary to get the path from
1406
1407        Example:
1408
1409        ```
1410
1411        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1412        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1413        ```
1414        """
1415        if len(data) == 0:
1416            raise Exception("Attempting to pluck from an empty list")
1417        curried_fn = self.curry(fn)
1418        if curried_fn.__arity__ != 1:
1419            raise Exception(
1420                "`pluckIf` `fn` must have an arity of 1 (take one input)"
1421            )
1422        if isinstance(path, str):
1423            path = [path]
1424        return [
1425            __pathOr__(default=None, path=path, data=i) for i in data if fn(i)
1426        ]

Function:

  • Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value

Requires:

  • fn:
    • Type: function
    • What: A function to take in each item in data and return a boolean
    • Note: Only items that return true are plucked
    • Note: Should be a unary function (take one input)
  • path:
    • Type: list of strs
    • What: The path to pull given the data
    • Note: If a string is passed, assumes a single item path list with that string
  • data:
    • Type: list of dicts
    • What: A list of dictionary to get the path from

Example:


data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
def project(self, keys: list[str], data: list[dict]):
1428    def project(self, keys: list[str], data: list[dict]):
1429        """
1430        Function:
1431
1432        - Returns a list of dictionaries with only the keys provided
1433        - Analogous to SQL's `SELECT` statement
1434
1435        Requires:
1436
1437        - `keys`:
1438            - Type: list of strs
1439            - What: The keys to select from each dictionary in the data list
1440        - `data`:
1441            - Type: list of dicts
1442            - What: The list of dictionaries to select from
1443
1444        Example:
1445
1446        ```
1447        data=[
1448            {'a':1,'b':2,'c':3},
1449            {'a':4,'b':5,'c':6}
1450        ]
1451        pamda.project(keys=['a','c'], data=data)
1452        #=> [
1453        #=>     {'a':1,'c':3},
1454        #=>     {'a':4,'c':6}
1455        #=> ]
1456        ```
1457        """
1458        return [{key: record[key] for key in keys} for record in data]

Function:

  • Returns a list of dictionaries with only the keys provided
  • Analogous to SQL's SELECT statement

Requires:

  • keys:
    • Type: list of strs
    • What: The keys to select from each dictionary in the data list
  • data:
    • Type: list of dicts
    • What: The list of dictionaries to select from

Example:

data=[
    {'a':1,'b':2,'c':3},
    {'a':4,'b':5,'c':6}
]
pamda.project(keys=['a','c'], data=data)
#=> [
#=>     {'a':1,'c':3},
#=>     {'a':4,'c':6}
#=> ]
def props(self, keys: list[str], data: dict):
1460    def props(self, keys: list[str], data: dict):
1461        """
1462        Function:
1463
1464        - Returns the values of a list of keys within a dictionary
1465
1466        Requires:
1467
1468        - `keys`:
1469            - Type: list of strs
1470            - What: The keys to pull given the data
1471        - `data`:
1472            - Type: dict
1473            - What: A dictionary to get the keys from
1474
1475        Example:
1476        ```
1477        data={'a':1,'b':2,'c':3}
1478        pamda.props(keys=['a','c'], data=data)
1479        #=> [1,3]
1480        ```
1481        """
1482        return [data[key] for key in keys]

Function:

  • Returns the values of a list of keys within a dictionary

Requires:

  • keys:
    • Type: list of strs
    • What: The keys to pull given the data
  • data:
    • Type: dict
    • What: A dictionary to get the keys from

Example:

data={'a':1,'b':2,'c':3}
pamda.props(keys=['a','c'], data=data)
#=> [1,3]
def reduce(self, fn, initial_accumulator, data: list):
1484    def reduce(self, fn, initial_accumulator, data: list):
1485        """
1486        Function:
1487
1488        - Returns a single item by iterating a function starting with an accumulator over a list
1489
1490        Requires:
1491
1492        - `fn`:
1493            - Type: function | method
1494            - What: The function or method to reduce
1495            - Note: This function should have an arity of 2 (take two inputs)
1496            - Note: The first input should take the accumulator value
1497            - Note: The second input should take the data value
1498        -`initial_accumulator`:
1499            - Type: any
1500            - What: The initial item to pass into the function when starting the accumulation process
1501        - `data`:
1502            - Type: list
1503            - What: The list of items to iterate over
1504
1505        Example:
1506
1507        ```
1508        data=[1,2,3,4]
1509        pamda.reduce(
1510            fn=pamda.add,
1511            initial_accumulator=0,
1512            data=data
1513        )
1514        #=> 10
1515
1516        ```
1517        """
1518        fn = self.curry(fn)
1519        if fn.__arity__ != 2:
1520            raise Exception(
1521                "`reduce` `fn` must have an arity of 2 (take two inputs)"
1522            )
1523        if not isinstance(data, (list)):
1524            raise Exception("`reduce` `data` must be a list")
1525        if not len(data) > 0:
1526            raise Exception(
1527                "`reduce` `data` has a length of 0, however it must have a length of at least 1"
1528            )
1529        acc = initial_accumulator
1530        for i in data:
1531            acc = fn(acc, i)
1532        return acc

Function:

  • Returns a single item by iterating a function starting with an accumulator over a list

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to reduce
    • Note: This function should have an arity of 2 (take two inputs)
    • Note: The first input should take the accumulator value
    • Note: The second input should take the data value -initial_accumulator:
    • Type: any
    • What: The initial item to pass into the function when starting the accumulation process
  • data:
    • Type: list
    • What: The list of items to iterate over

Example:

data=[1,2,3,4]
pamda.reduce(
    fn=pamda.add,
    initial_accumulator=0,
    data=data
)
#=> 10

def safeDivide(self, denominator: int | float, a: int | float):
1534    def safeDivide(self, denominator: int | float, a: int | float):
1535        """
1536        Function:
1537
1538        - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
1539
1540        Requires:
1541
1542        - `denominator`:
1543            - Type: int | float
1544            - What: The denominator
1545
1546        - `a`:
1547            - Type: int | float
1548            - What: The numerator
1549
1550        Example:
1551
1552        ```
1553        pamda.safeDivide(2,10) #=> 5
1554        pamda.safeDivide(0,10) #=> 10
1555        ```
1556        """
1557        return a / denominator if denominator != 0 else a

Function:

  • Forces division to work by enforcing a denominator of 1 if the provided denominator is zero

Requires:

  • denominator:

    • Type: int | float
    • What: The denominator
  • a:

    • Type: int | float
    • What: The numerator

Example:

pamda.safeDivide(2,10) #=> 5
pamda.safeDivide(0,10) #=> 10
def safeDivideDefault( self, default_denominator: int | float, denominator: int | float, a: int | float):
1559    def safeDivideDefault(
1560        self,
1561        default_denominator: int | float,
1562        denominator: int | float,
1563        a: int | float,
1564    ):
1565        """
1566        Function:
1567
1568        - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
1569
1570        Requires:
1571
1572        - `default_denominator`:
1573            - Type: int | float
1574            - What: A non zero denominator to use if denominator is zero
1575            - Default: 1
1576        - `denominator`:
1577            - Type: int | float
1578            - What: The denominator
1579        - `a`:
1580            - Type: int | float
1581            - What: The numerator
1582
1583        Example:
1584
1585        ```
1586        pamda.safeDivideDefault(2,5,10) #=> 2
1587        pamda.safeDivideDefault(2,0,10) #=> 5
1588        ```
1589        """
1590        if default_denominator == 0:
1591            raise Exception(
1592                "`safeDivideDefault` `default_denominator` can not be 0"
1593            )
1594        return a / denominator if denominator != 0 else a / default_denominator

Function:

  • Forces division to work by enforcing a non zero default denominator if the provided denominator is zero

Requires:

  • default_denominator:
    • Type: int | float
    • What: A non zero denominator to use if denominator is zero
    • Default: 1
  • denominator:
    • Type: int | float
    • What: The denominator
  • a:
    • Type: int | float
    • What: The numerator

Example:

pamda.safeDivideDefault(2,5,10) #=> 2
pamda.safeDivideDefault(2,0,10) #=> 5
def symmetricDifference(self, a: list, b: list):
1596    def symmetricDifference(self, a: list, b: list):
1597        """
1598        Function:
1599
1600        - Combines two lists into a list of no duplicates items present in one list but not the other
1601
1602        Requires:
1603
1604        - `a`:
1605            - Type: list
1606            - What: List of items in which to look for a difference
1607        - `b`:
1608            - Type: list
1609            - What: List of items in which to look for a difference
1610
1611        Example:
1612
1613        ```
1614        a=['a','b']
1615        b=['b','c']
1616        pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1617        ```
1618        """
1619        return list(set(a).difference(set(b))) + list(set(b).difference(set(a)))

Function:

  • Combines two lists into a list of no duplicates items present in one list but not the other

Requires:

  • a:
    • Type: list
    • What: List of items in which to look for a difference
  • b:
    • Type: list
    • What: List of items in which to look for a difference

Example:

a=['a','b']
b=['b','c']
pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
def tail(self, data: list | str):
1621    def tail(self, data: list | str):
1622        """
1623        Function:
1624
1625        - Picks the last item out of a list or string
1626
1627        Requires:
1628
1629        - `data`:
1630            - Type: list | str
1631            - What: A list or string
1632
1633        Example:
1634
1635        ```
1636        data=['fe','fi','fo','fum']
1637        pamda.tail(
1638            data=data
1639        ) #=> fum
1640        ```
1641        """
1642        if not len(data) > 0:
1643            raise Exception("Attempting to call `tail` on an empty list or str")
1644        return data[-1]

Function:

  • Picks the last item out of a list or string

Requires:

  • data:
    • Type: list | str
    • What: A list or string

Example:

data=['fe','fi','fo','fum']
pamda.tail(
    data=data
) #=> fum
def thunkify(self, fn):
1646    def thunkify(self, fn):
1647        """
1648        Function:
1649
1650        - Creates a curried thunk out of a function
1651        - Evaluation of the thunk lazy and is delayed until called
1652
1653        Requires:
1654
1655        - `fn`:
1656            - Type: function | method
1657            - What: The function or method to thunkify
1658            - Note: Thunkified functions are automatically curried
1659            - Note: Class methods auto apply self during thunkify
1660
1661        Notes:
1662
1663        - Input functions are not thunkified in place
1664        - The returned function is a thunkified version of the input function
1665        - A curried function can be thunkified in place by calling fn.thunkify()
1666
1667        Examples:
1668
1669        ```
1670        def add(a,b):
1671            return a+b
1672
1673        addThunk=pamda.thunkify(add)
1674
1675        add(1,2) #=> 3
1676        addThunk(1,2)
1677        addThunk(1,2)() #=> 3
1678
1679        x=addThunk(1,2)
1680        x() #=> 3
1681        ```
1682
1683        ```
1684        @pamda.curry
1685        def add(a,b):
1686            return a+b
1687
1688        add(1,2) #=> 3
1689
1690        add.thunkify()
1691
1692        add(1,2)
1693        add(1,2)() #=> 3
1694        ```
1695        """
1696        fn = self.curry(fn)
1697        return fn.thunkify()

Function:

  • Creates a curried thunk out of a function
  • Evaluation of the thunk lazy and is delayed until called

Requires:

  • fn:
    • Type: function | method
    • What: The function or method to thunkify
    • Note: Thunkified functions are automatically curried
    • Note: Class methods auto apply self during thunkify

Notes:

  • Input functions are not thunkified in place
  • The returned function is a thunkified version of the input function
  • A curried function can be thunkified in place by calling fn.thunkify()

Examples:

def add(a,b):
    return a+b

addThunk=pamda.thunkify(add)

add(1,2) #=> 3
addThunk(1,2)
addThunk(1,2)() #=> 3

x=addThunk(1,2)
x() #=> 3
@pamda.curry
def add(a,b):
    return a+b

add(1,2) #=> 3

add.thunkify()

add(1,2)
add(1,2)() #=> 3
def unnest(self, data: list):
1699    def unnest(self, data: list):
1700        """
1701        Function:
1702
1703        - Removes one level of depth for all items in a list
1704
1705        Requires:
1706
1707        - `data`:
1708            - Type: list
1709            - What: A list of items to unnest by one level
1710
1711        Examples:
1712
1713        ```
1714        data=['fe','fi',['fo',['fum']]]
1715        pamda.unnest(
1716            data=data
1717        ) #=> ['fe','fi','fo',['fum']]
1718        ```
1719        """
1720        if not len(data) > 0:
1721            raise Exception("Attempting to call `unnest` on an empty list")
1722        output = []
1723        for i in data:
1724            if isinstance(i, list):
1725                output += i
1726            else:
1727                output.append(i)
1728        return output

Function:

  • Removes one level of depth for all items in a list

Requires:

  • data:
    • Type: list
    • What: A list of items to unnest by one level

Examples:

data=['fe','fi',['fo',['fum']]]
pamda.unnest(
    data=data
) #=> ['fe','fi','fo',['fum']]
def zip(self, a: list, b: list):
1730    def zip(self, a: list, b: list):
1731        """
1732        Function:
1733
1734        - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
1735
1736        Requires:
1737
1738        - `a`:
1739            - Type: list
1740            - What: List of items to appear in new list first
1741        - `b`:
1742            - Type: list
1743            - What: List of items to appear in new list second
1744
1745        Example:
1746
1747        ```
1748        a=['a','b']
1749        b=[1,2]
1750        pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1751        ```
1752        """
1753        return list(map(list, zip(a, b)))

Function:

  • Creates a new list out of the two supplied by pairing up equally-positioned items from both lists

Requires:

  • a:
    • Type: list
    • What: List of items to appear in new list first
  • b:
    • Type: list
    • What: List of items to appear in new list second

Example:

a=['a','b']
b=[1,2]
pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
def zipObj(self, a: list, b: list):
1755    def zipObj(self, a: list, b: list):
1756        """
1757        Function:
1758
1759        - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
1760        - The first list represents keys and the second values
1761
1762        Requires:
1763
1764        - `a`:
1765            - Type: list
1766            - What: List of items to appear in new list first
1767        - `b`:
1768            - Type: list
1769            - What: List of items to appear in new list second
1770
1771        Example:
1772
1773        ```
1774        a=['a','b']
1775        b=[1,2]
1776        pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}
1777        ```
1778        """
1779        return dict(zip(a, b))

Function:

  • Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
  • The first list represents keys and the second values

Requires:

  • a:
    • Type: list
    • What: List of items to appear in new list first
  • b:
    • Type: list
    • What: List of items to appear in new list second

Example:

a=['a','b']
b=[1,2]
pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}