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        return path[-1] in reduce(lambda x, y: x.get(y, {}), path[:-1], data)
 846
 847    def hardRound(self, decimal_places: int, a: int | float):
 848        """
 849        Function:
 850
 851        - Rounds to a set number of decimal places regardless of floating point math in python
 852
 853        Requires:
 854
 855        - `decimal_places`:
 856            - Type: int
 857            - What: The number of decimal places to round to
 858            - Default: 0
 859            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
 860        - `a`:
 861            - Type: int | float
 862            - What: The number to round
 863
 864        Example:
 865
 866        ```
 867        a=12.345
 868        pamda.hardRound(1,a) #=> 12.3
 869        pamda.hardRound(-1,a) #=> 10
 870        ```
 871        """
 872        return int(a * (10**decimal_places) + 0.5) / (10**decimal_places)
 873
 874    def head(self, data: list | str):
 875        """
 876        Function:
 877
 878        - Picks the first item out of a list or string
 879
 880        Requires:
 881
 882        - `data`:
 883            - Type: list | str
 884            - What: A list or string
 885
 886        Example:
 887
 888        ```
 889        data=['fe','fi','fo','fum']
 890        pamda.first(
 891            data=data
 892        ) #=> fe
 893        ```
 894        """
 895        if not isinstance(data, (list, str)):
 896            raise Exception("`head` can only be called on a `str` or a `list`")
 897        if not len(data) > 0:
 898            raise Exception("Attempting to call `head` on an empty list or str")
 899        return data[0]
 900
 901    def inc(self, a: int | float):
 902        """
 903        Function:
 904
 905        - Increments a number by one
 906
 907        Requires:
 908
 909        - `a`:
 910            - Type: int | float
 911            - What: The number to increment
 912
 913        Example:
 914
 915        ```
 916        pamda.inc(42) #=> 43
 917        ```
 918        """
 919        if not isinstance(a, (int, float)):
 920            raise Exception("`a` must be an `int` or a `float`")
 921        return a + 1
 922
 923    def intersection(self, a: list, b: list):
 924        """
 925        Function:
 926
 927        - Combines two lists into a list of no duplicates composed of those elements common to both lists
 928
 929        Requires:
 930
 931        - `a`:
 932            - Type: list
 933            - What: List of items in which to look for an intersection
 934        - `b`:
 935            - Type: list
 936            - What: List of items in which to look for an intersection
 937
 938        Example:
 939
 940        ```
 941        a=['a','b']
 942        b=['b','c']
 943        pamda.intersection(a=a, b=b) #=> ['b']
 944        ```
 945        """
 946        return list(set(a).intersection(set(b)))
 947
 948    def map(self, fn, data: list | dict):
 949        """
 950        Function:
 951
 952        - Maps a function over a list or a dictionary
 953
 954        Requires:
 955
 956        - `fn`:
 957            - Type: function | method
 958            - What: The function or method to map over the list or dictionary
 959            - Note: This function should have an arity of 1
 960        - `data`:
 961            - Type: list | dict
 962            - What: The list or dict of items to map the function over
 963
 964        Examples:
 965
 966        ```
 967        data=[1,2,3]
 968        pamda.map(
 969            fn=pamda.inc,
 970            data=data
 971        )
 972        #=> [2,3,4]
 973        ```
 974
 975        ```
 976        data={'a':1,'b':2,'c':3}
 977        pamda.map(
 978            fn=pamda.inc,
 979            data=data
 980        )
 981        #=> {'a':2,'b':3,'c':4}
 982        ```
 983
 984        """
 985        # TODO: Check for efficiency gains
 986        fn = self.curry(fn)
 987        if fn.__arity__ != 1:
 988            raise Exception("`map` `fn` must be unary (take one input)")
 989        if not len(data) > 0:
 990            raise Exception(
 991                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
 992            )
 993        if isinstance(data, dict):
 994            return {key: fn(value) for key, value in data.items()}
 995        else:
 996            return [fn(i) for i in data]
 997
 998    def mean(self, data: list):
 999        """
1000        Function:
1001
1002        - Calculates the mean of a given list
1003
1004        Requires:
1005
1006        - `data`:
1007            - Type: list of (floats | ints)
1008            - What: The list with wich to calculate the mean
1009            - Note: If the length of this list is 0, returns None
1010
1011        Example:
1012
1013        ```
1014        data=[1,2,3]
1015        pamda.mean(data=data)
1016        #=> 2
1017        ```
1018
1019        ```
1020        data=[]
1021        pamda.mean(data=data)
1022        #=> None
1023        ```
1024        """
1025        if len(data) == 0:
1026            return None
1027        return sum(data) / len(data)
1028
1029    def median(self, data: list):
1030        """
1031        Function:
1032
1033        - Calculates the median of a given list
1034        - If the length of the list is even, calculates the mean of the two central values
1035
1036        Requires:
1037
1038        - `data`:
1039            - Type: list of (floats | ints)
1040            - What: The list with wich to calculate the mean
1041            - Note: If the length of this list is 0, returns None
1042
1043        Examples:
1044
1045        ```
1046        data=[7,2,8,9]
1047        pamda.median(data=data)
1048        #=> 7.5
1049        ```
1050
1051        ```
1052        data=[7,8,9]
1053        pamda.median(data=data)
1054        #=> 8
1055        ```
1056
1057        ```
1058        data=[]
1059        pamda.median(data=data)
1060        #=> None
1061        ```
1062        """
1063        if not isinstance(data, (list)):
1064            raise Exception("`median` `data` must be a list")
1065        length = len(data)
1066        if length == 0:
1067            return None
1068        data = sorted(data)
1069        if length % 2 == 0:
1070            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1071        return data[int(length / 2)]
1072
1073    def mergeDeep(self, update_data, data):
1074        """
1075        Function:
1076
1077        - Recursively merges two nested dictionaries keeping all keys at each layer
1078        - Values from `update_data` are used when keys are present in both dictionaries
1079
1080        Requires:
1081
1082        - `update_data`:
1083            - Type: any
1084            - What: The new data that will take precedence during merging
1085        - `data`:
1086            - Type: any
1087            - What: The original data that will be merged into
1088
1089        Example:
1090
1091        ```
1092        data={'a':{'b':{'c':'d'},'e':'f'}}
1093        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1094        pamda.mergeDeep(
1095            update_data=update_data,
1096            data=data
1097        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1098        ```
1099        """
1100        return __mergeDeep__(update_data, data)
1101
1102    def nest(self, path_keys: list, value_key: str, data: list):
1103        """
1104        Function:
1105
1106        - Nests a list of dictionaries into a nested dictionary
1107        - Similar items are appended to a list in the end of the nested dictionary
1108
1109        Requires:
1110
1111        - `path_keys`:
1112            - Type: list of strs
1113            - What: The variables to pull from each item in data
1114            - Note: Used to build out the nested dicitonary
1115            - Note: Order matters as the nesting occurs in order of variable
1116        - `value_key`:
1117            - Type: str
1118            - What: The variable to add to the list at the end of the nested dictionary path
1119        - `data`:
1120            - Type: list of dicts
1121            - What: A list of dictionaries to use for nesting purposes
1122
1123        Example:
1124
1125        ```
1126        data=[
1127            {'x_1':'a','x_2':'b', 'output':'c'},
1128            {'x_1':'a','x_2':'b', 'output':'d'},
1129            {'x_1':'a','x_2':'e', 'output':'f'}
1130        ]
1131        pamda.nest(
1132            path_keys=['x_1','x_2'],
1133            value_key='output',
1134            data=data
1135        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1136        ```
1137        """
1138        if not isinstance(data, list):
1139            raise Exception("Attempting to `nest` an object that is not a list")
1140        if len(data) == 0:
1141            raise Exception("Attempting to `nest` from an empty list")
1142        nested_output = {}
1143        for item in self.groupKeys(keys=path_keys, data=data):
1144            nested_output = __assocPath__(
1145                path=__getKeyValues__(path_keys, item[0]),
1146                value=[i.get(value_key) for i in item],
1147                data=nested_output,
1148            )
1149        return nested_output
1150
1151    def nestItem(self, path_keys: list, data: list):
1152        """
1153        Function:
1154
1155        - Nests a list of dictionaries into a nested dictionary
1156        - Similar items are appended to a list in the end of the nested dictionary
1157        - Similar to `nest`, except no values are plucked for the aggregated list
1158
1159        Requires:
1160
1161        - `path_keys`:
1162            - Type: list of strs
1163            - What: The variables to pull from each item in data
1164            - Note: Used to build out the nested dicitonary
1165            - Note: Order matters as the nesting occurs in order of variable
1166        - `data`:
1167            - Type: list of dicts
1168            - What: A list of dictionaries to use for nesting purposes
1169
1170        Example:
1171
1172        ```
1173        data=[
1174            {'x_1':'a','x_2':'b'},
1175            {'x_1':'a','x_2':'b'},
1176            {'x_1':'a','x_2':'e'}
1177        ]
1178        pamda.nestItem
1179            path_keys=['x_1','x_2'],
1180            data=data
1181        )
1182        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1183
1184        ```
1185        """
1186        if not isinstance(data, list):
1187            raise Exception("Attempting to `nest` an object that is not a list")
1188        if len(data) == 0:
1189            raise Exception("Attempting to `nest` from an empty list")
1190        nested_output = {}
1191        for item in self.groupKeys(keys=path_keys, data=data):
1192            nested_output = __assocPath__(
1193                path=__getKeyValues__(path_keys, item[0]),
1194                value=item,
1195                data=nested_output,
1196            )
1197        return nested_output
1198
1199    def path(self, path: list | str, data: dict):
1200        """
1201        Function:
1202
1203        - Returns the value of a path within a nested dictionary or None if the path does not exist
1204
1205        Requires:
1206
1207        - `path`:
1208            - Type: list of strs | str
1209            - What: The path to pull given the data
1210            - Note: If a string is passed, assumes a single item path list with that string
1211        - `data`:
1212            - Type: dict
1213            - What: A dictionary to get the path from
1214
1215        Example:
1216
1217        ```
1218        data={'a':{'b':1}}
1219        pamda.path(path=['a','b'], data=data) #=> 1
1220        ```
1221        """
1222        if isinstance(path, str):
1223            path = [path]
1224        return __pathOr__(None, path, data)
1225
1226    def pathOr(self, default, path: list | str, data: dict):
1227        """
1228        Function:
1229
1230        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1231
1232        Requires:
1233
1234        - `default`:
1235            - Type: any
1236            - What: The object to return if the path does not exist
1237        - `path`:
1238            - Type: list of strs | str
1239            - What: The path to pull given the data
1240            - Note: If a string is passed, assumes a single item path list with that string
1241        - `data`:
1242            - Type: dict
1243            - What: A dictionary to get the path from
1244
1245        Example:
1246
1247        ```
1248        data={'a':{'b':1}}
1249        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1250        ```
1251        """
1252        if isinstance(path, str):
1253            path = [path]
1254        return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get(
1255            path[-1], default
1256        )
1257
1258    def pipe(self, fns: list, args: tuple, kwargs: dict):
1259        """
1260        Function:
1261
1262        - Pipes data through n functions in order (left to right composition) and returns the output
1263
1264        Requires:
1265
1266        - `fns`:
1267            - Type: list of (functions | methods)
1268            - What: The list of functions and methods to pipe the data through
1269            - Notes: The first function in the list can be any arity (accepting any number of inputs)
1270            - Notes: Any further function in the list can only be unary (single input)
1271            - Notes: A function can be curried, but is not required to be
1272            - Notes: You may opt to curry functions and add inputs to make them unary
1273        - `args`:
1274            - Type: tuple
1275            - What: a tuple of positional arguments to pass to the first function in `fns`
1276        - `kwargs`:
1277            - Type: dict
1278            - What: a dictionary of keyword arguments to pass to the first function in `fns`
1279
1280        Examples:
1281
1282        ```
1283        data=['abc','def']
1284        pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
1285        pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
1286        ```
1287
1288        ```
1289        data={'a':{'b':'c'}}
1290        curriedPath=pamda.curry(pamda.path)
1291        pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1292        ```
1293        """
1294        if len(fns) == 0:
1295            raise Exception("`fns` must be a list with at least one function")
1296        if self.getArity(fns[0]) == 0:
1297            raise Exception(
1298                "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0."
1299            )
1300        if not all([(self.getArity(fn) == 1) for fn in fns[1:]]):
1301            raise Exception(
1302                "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)."
1303            )
1304        out = fns[0](*args, **kwargs)
1305        for fn in fns[1:]:
1306            out = fn(out)
1307        return out
1308
1309    def pivot(self, data: list[dict] | dict[Any, list]):
1310        """
1311        Function:
1312
1313        - Pivots a list of dictionaries into a dictionary of lists
1314        - Pivots a dictionary of lists into a list of dictionaries
1315
1316        Requires:
1317
1318        - `data`:
1319            - Type: list of dicts | dict of lists
1320            - What: The data to pivot
1321            - Note: If a list of dictionaries is passed, all dictionaries must have the same keys
1322            - Note: If a dictionary of lists is passed, all lists must have the same length
1323
1324        Example:
1325
1326        ```
1327        data=[
1328            {'a':1,'b':2},
1329            {'a':3,'b':4}
1330        ]
1331        pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
1332
1333        data={'a':[1,3],'b':[2,4]}
1334        pamda.pivot(data=data)
1335        #=> [
1336        #=>     {'a':1,'b':2},
1337        #=>     {'a':3,'b':4}
1338        #=> ]
1339        ```
1340        """
1341        if isinstance(data, list):
1342            return {
1343                key: [record[key] for record in data] for key in data[0].keys()
1344            }
1345        else:
1346            return [
1347                {key: data[key][i] for key in data.keys()}
1348                for i in range(len(data[list(data.keys())[0]]))
1349            ]
1350
1351    def pluck(self, path: list | str, data: list):
1352        """
1353        Function:
1354
1355        - Returns the values of a path within a list of nested dictionaries
1356
1357        Requires:
1358
1359        - `path`:
1360            - Type: list of strs
1361            - What: The path to pull given the data
1362            - Note: If a string is passed, assumes a single item path list with that string
1363        - `data`:
1364            - Type: list of dicts
1365            - What: A list of dictionaries to get the path from
1366
1367        Example:
1368
1369        ```
1370        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1371        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1372        ```
1373        """
1374        if len(data) == 0:
1375            raise Exception("Attempting to pluck from an empty list")
1376        if isinstance(path, str):
1377            path = [path]
1378        return [__pathOr__(default=None, path=path, data=i) for i in data]
1379
1380    def pluckIf(self, fn, path: list | str, data: list):
1381        """
1382        Function:
1383
1384        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1385
1386        Requires:
1387
1388        - `fn`:
1389            - Type: function
1390            - What: A function to take in each item in data and return a boolean
1391            - Note: Only items that return true are plucked
1392            - Note: Should be a unary function (take one input)
1393        - `path`:
1394            - Type: list of strs
1395            - What: The path to pull given the data
1396            - Note: If a string is passed, assumes a single item path list with that string
1397        - `data`:
1398            - Type: list of dicts
1399            - What: A list of dictionary to get the path from
1400
1401        Example:
1402
1403        ```
1404
1405        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1406        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1407        ```
1408        """
1409        if len(data) == 0:
1410            raise Exception("Attempting to pluck from an empty list")
1411        curried_fn = self.curry(fn)
1412        if curried_fn.__arity__ != 1:
1413            raise Exception(
1414                "`pluckIf` `fn` must have an arity of 1 (take one input)"
1415            )
1416        if isinstance(path, str):
1417            path = [path]
1418        return [
1419            __pathOr__(default=None, path=path, data=i) for i in data if fn(i)
1420        ]
1421
1422    def project(self, keys: list[str], data: list[dict]):
1423        """
1424        Function:
1425
1426        - Returns a list of dictionaries with only the keys provided
1427        - Analogous to SQL's `SELECT` statement
1428
1429        Requires:
1430
1431        - `keys`:
1432            - Type: list of strs
1433            - What: The keys to select from each dictionary in the data list
1434        - `data`:
1435            - Type: list of dicts
1436            - What: The list of dictionaries to select from
1437
1438        Example:
1439
1440        ```
1441        data=[
1442            {'a':1,'b':2,'c':3},
1443            {'a':4,'b':5,'c':6}
1444        ]
1445        pamda.project(keys=['a','c'], data=data)
1446        #=> [
1447        #=>     {'a':1,'c':3},
1448        #=>     {'a':4,'c':6}
1449        #=> ]
1450        ```
1451        """
1452        return [{key: record[key] for key in keys} for record in data]
1453
1454    def props(self, keys: list[str], data: dict):
1455        """
1456        Function:
1457
1458        - Returns the values of a list of keys within a dictionary
1459
1460        Requires:
1461
1462        - `keys`:
1463            - Type: list of strs
1464            - What: The keys to pull given the data
1465        - `data`:
1466            - Type: dict
1467            - What: A dictionary to get the keys from
1468
1469        Example:
1470        ```
1471        data={'a':1,'b':2,'c':3}
1472        pamda.props(keys=['a','c'], data=data)
1473        #=> [1,3]
1474        ```
1475        """
1476        return [data[key] for key in keys]
1477
1478    def reduce(self, fn, initial_accumulator, data: list):
1479        """
1480        Function:
1481
1482        - Returns a single item by iterating a function starting with an accumulator over a list
1483
1484        Requires:
1485
1486        - `fn`:
1487            - Type: function | method
1488            - What: The function or method to reduce
1489            - Note: This function should have an arity of 2 (take two inputs)
1490            - Note: The first input should take the accumulator value
1491            - Note: The second input should take the data value
1492        -`initial_accumulator`:
1493            - Type: any
1494            - What: The initial item to pass into the function when starting the accumulation process
1495        - `data`:
1496            - Type: list
1497            - What: The list of items to iterate over
1498
1499        Example:
1500
1501        ```
1502        data=[1,2,3,4]
1503        pamda.reduce(
1504            fn=pamda.add,
1505            initial_accumulator=0,
1506            data=data
1507        )
1508        #=> 10
1509
1510        ```
1511        """
1512        fn = self.curry(fn)
1513        if fn.__arity__ != 2:
1514            raise Exception(
1515                "`reduce` `fn` must have an arity of 2 (take two inputs)"
1516            )
1517        if not isinstance(data, (list)):
1518            raise Exception("`reduce` `data` must be a list")
1519        if not len(data) > 0:
1520            raise Exception(
1521                "`reduce` `data` has a length of 0, however it must have a length of at least 1"
1522            )
1523        acc = initial_accumulator
1524        for i in data:
1525            acc = fn(acc, i)
1526        return acc
1527
1528    def safeDivide(self, denominator: int | float, a: int | float):
1529        """
1530        Function:
1531
1532        - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
1533
1534        Requires:
1535
1536        - `denominator`:
1537            - Type: int | float
1538            - What: The denominator
1539
1540        - `a`:
1541            - Type: int | float
1542            - What: The numerator
1543
1544        Example:
1545
1546        ```
1547        pamda.safeDivide(2,10) #=> 5
1548        pamda.safeDivide(0,10) #=> 10
1549        ```
1550        """
1551        return a / denominator if denominator != 0 else a
1552
1553    def safeDivideDefault(
1554        self,
1555        default_denominator: int | float,
1556        denominator: int | float,
1557        a: int | float,
1558    ):
1559        """
1560        Function:
1561
1562        - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
1563
1564        Requires:
1565
1566        - `default_denominator`:
1567            - Type: int | float
1568            - What: A non zero denominator to use if denominator is zero
1569            - Default: 1
1570        - `denominator`:
1571            - Type: int | float
1572            - What: The denominator
1573        - `a`:
1574            - Type: int | float
1575            - What: The numerator
1576
1577        Example:
1578
1579        ```
1580        pamda.safeDivideDefault(2,5,10) #=> 2
1581        pamda.safeDivideDefault(2,0,10) #=> 5
1582        ```
1583        """
1584        if default_denominator == 0:
1585            raise Exception(
1586                "`safeDivideDefault` `default_denominator` can not be 0"
1587            )
1588        return a / denominator if denominator != 0 else a / default_denominator
1589
1590    def symmetricDifference(self, a: list, b: list):
1591        """
1592        Function:
1593
1594        - Combines two lists into a list of no duplicates items present in one list but not the other
1595
1596        Requires:
1597
1598        - `a`:
1599            - Type: list
1600            - What: List of items in which to look for a difference
1601        - `b`:
1602            - Type: list
1603            - What: List of items in which to look for a difference
1604
1605        Example:
1606
1607        ```
1608        a=['a','b']
1609        b=['b','c']
1610        pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1611        ```
1612        """
1613        return list(set(a).difference(set(b))) + list(set(b).difference(set(a)))
1614
1615    def tail(self, data: list | str):
1616        """
1617        Function:
1618
1619        - Picks the last item out of a list or string
1620
1621        Requires:
1622
1623        - `data`:
1624            - Type: list | str
1625            - What: A list or string
1626
1627        Example:
1628
1629        ```
1630        data=['fe','fi','fo','fum']
1631        pamda.tail(
1632            data=data
1633        ) #=> fum
1634        ```
1635        """
1636        if not len(data) > 0:
1637            raise Exception("Attempting to call `tail` on an empty list or str")
1638        return data[-1]
1639
1640    def thunkify(self, fn):
1641        """
1642        Function:
1643
1644        - Creates a curried thunk out of a function
1645        - Evaluation of the thunk lazy and is delayed until called
1646
1647        Requires:
1648
1649        - `fn`:
1650            - Type: function | method
1651            - What: The function or method to thunkify
1652            - Note: Thunkified functions are automatically curried
1653            - Note: Class methods auto apply self during thunkify
1654
1655        Notes:
1656
1657        - Input functions are not thunkified in place
1658        - The returned function is a thunkified version of the input function
1659        - A curried function can be thunkified in place by calling fn.thunkify()
1660
1661        Examples:
1662
1663        ```
1664        def add(a,b):
1665            return a+b
1666
1667        addThunk=pamda.thunkify(add)
1668
1669        add(1,2) #=> 3
1670        addThunk(1,2)
1671        addThunk(1,2)() #=> 3
1672
1673        x=addThunk(1,2)
1674        x() #=> 3
1675        ```
1676
1677        ```
1678        @pamda.curry
1679        def add(a,b):
1680            return a+b
1681
1682        add(1,2) #=> 3
1683
1684        add.thunkify()
1685
1686        add(1,2)
1687        add(1,2)() #=> 3
1688        ```
1689        """
1690        fn = self.curry(fn)
1691        return fn.thunkify()
1692
1693    def unnest(self, data: list):
1694        """
1695        Function:
1696
1697        - Removes one level of depth for all items in a list
1698
1699        Requires:
1700
1701        - `data`:
1702            - Type: list
1703            - What: A list of items to unnest by one level
1704
1705        Examples:
1706
1707        ```
1708        data=['fe','fi',['fo',['fum']]]
1709        pamda.unnest(
1710            data=data
1711        ) #=> ['fe','fi','fo',['fum']]
1712        ```
1713        """
1714        if not len(data) > 0:
1715            raise Exception("Attempting to call `unnest` on an empty list")
1716        output = []
1717        for i in data:
1718            if isinstance(i, list):
1719                output += i
1720            else:
1721                output.append(i)
1722        return output
1723
1724    def zip(self, a: list, b: list):
1725        """
1726        Function:
1727
1728        - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
1729
1730        Requires:
1731
1732        - `a`:
1733            - Type: list
1734            - What: List of items to appear in new list first
1735        - `b`:
1736            - Type: list
1737            - What: List of items to appear in new list second
1738
1739        Example:
1740
1741        ```
1742        a=['a','b']
1743        b=[1,2]
1744        pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1745        ```
1746        """
1747        return list(map(list, zip(a, b)))
1748
1749    def zipObj(self, a: list, b: list):
1750        """
1751        Function:
1752
1753        - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
1754        - The first list represents keys and the second values
1755
1756        Requires:
1757
1758        - `a`:
1759            - Type: list
1760            - What: List of items to appear in new list first
1761        - `b`:
1762            - Type: list
1763            - What: List of items to appear in new list second
1764
1765        Example:
1766
1767        ```
1768        a=['a','b']
1769        b=[1,2]
1770        pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}
1771        ```
1772        """
1773        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        return path[-1] in reduce(lambda x, y: x.get(y, {}), path[:-1], data)
 847
 848    def hardRound(self, decimal_places: int, a: int | float):
 849        """
 850        Function:
 851
 852        - Rounds to a set number of decimal places regardless of floating point math in python
 853
 854        Requires:
 855
 856        - `decimal_places`:
 857            - Type: int
 858            - What: The number of decimal places to round to
 859            - Default: 0
 860            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
 861        - `a`:
 862            - Type: int | float
 863            - What: The number to round
 864
 865        Example:
 866
 867        ```
 868        a=12.345
 869        pamda.hardRound(1,a) #=> 12.3
 870        pamda.hardRound(-1,a) #=> 10
 871        ```
 872        """
 873        return int(a * (10**decimal_places) + 0.5) / (10**decimal_places)
 874
 875    def head(self, data: list | str):
 876        """
 877        Function:
 878
 879        - Picks the first item out of a list or string
 880
 881        Requires:
 882
 883        - `data`:
 884            - Type: list | str
 885            - What: A list or string
 886
 887        Example:
 888
 889        ```
 890        data=['fe','fi','fo','fum']
 891        pamda.first(
 892            data=data
 893        ) #=> fe
 894        ```
 895        """
 896        if not isinstance(data, (list, str)):
 897            raise Exception("`head` can only be called on a `str` or a `list`")
 898        if not len(data) > 0:
 899            raise Exception("Attempting to call `head` on an empty list or str")
 900        return data[0]
 901
 902    def inc(self, a: int | float):
 903        """
 904        Function:
 905
 906        - Increments a number by one
 907
 908        Requires:
 909
 910        - `a`:
 911            - Type: int | float
 912            - What: The number to increment
 913
 914        Example:
 915
 916        ```
 917        pamda.inc(42) #=> 43
 918        ```
 919        """
 920        if not isinstance(a, (int, float)):
 921            raise Exception("`a` must be an `int` or a `float`")
 922        return a + 1
 923
 924    def intersection(self, a: list, b: list):
 925        """
 926        Function:
 927
 928        - Combines two lists into a list of no duplicates composed of those elements common to both lists
 929
 930        Requires:
 931
 932        - `a`:
 933            - Type: list
 934            - What: List of items in which to look for an intersection
 935        - `b`:
 936            - Type: list
 937            - What: List of items in which to look for an intersection
 938
 939        Example:
 940
 941        ```
 942        a=['a','b']
 943        b=['b','c']
 944        pamda.intersection(a=a, b=b) #=> ['b']
 945        ```
 946        """
 947        return list(set(a).intersection(set(b)))
 948
 949    def map(self, fn, data: list | dict):
 950        """
 951        Function:
 952
 953        - Maps a function over a list or a dictionary
 954
 955        Requires:
 956
 957        - `fn`:
 958            - Type: function | method
 959            - What: The function or method to map over the list or dictionary
 960            - Note: This function should have an arity of 1
 961        - `data`:
 962            - Type: list | dict
 963            - What: The list or dict of items to map the function over
 964
 965        Examples:
 966
 967        ```
 968        data=[1,2,3]
 969        pamda.map(
 970            fn=pamda.inc,
 971            data=data
 972        )
 973        #=> [2,3,4]
 974        ```
 975
 976        ```
 977        data={'a':1,'b':2,'c':3}
 978        pamda.map(
 979            fn=pamda.inc,
 980            data=data
 981        )
 982        #=> {'a':2,'b':3,'c':4}
 983        ```
 984
 985        """
 986        # TODO: Check for efficiency gains
 987        fn = self.curry(fn)
 988        if fn.__arity__ != 1:
 989            raise Exception("`map` `fn` must be unary (take one input)")
 990        if not len(data) > 0:
 991            raise Exception(
 992                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
 993            )
 994        if isinstance(data, dict):
 995            return {key: fn(value) for key, value in data.items()}
 996        else:
 997            return [fn(i) for i in data]
 998
 999    def mean(self, data: list):
1000        """
1001        Function:
1002
1003        - Calculates the mean of a given list
1004
1005        Requires:
1006
1007        - `data`:
1008            - Type: list of (floats | ints)
1009            - What: The list with wich to calculate the mean
1010            - Note: If the length of this list is 0, returns None
1011
1012        Example:
1013
1014        ```
1015        data=[1,2,3]
1016        pamda.mean(data=data)
1017        #=> 2
1018        ```
1019
1020        ```
1021        data=[]
1022        pamda.mean(data=data)
1023        #=> None
1024        ```
1025        """
1026        if len(data) == 0:
1027            return None
1028        return sum(data) / len(data)
1029
1030    def median(self, data: list):
1031        """
1032        Function:
1033
1034        - Calculates the median of a given list
1035        - If the length of the list is even, calculates the mean of the two central values
1036
1037        Requires:
1038
1039        - `data`:
1040            - Type: list of (floats | ints)
1041            - What: The list with wich to calculate the mean
1042            - Note: If the length of this list is 0, returns None
1043
1044        Examples:
1045
1046        ```
1047        data=[7,2,8,9]
1048        pamda.median(data=data)
1049        #=> 7.5
1050        ```
1051
1052        ```
1053        data=[7,8,9]
1054        pamda.median(data=data)
1055        #=> 8
1056        ```
1057
1058        ```
1059        data=[]
1060        pamda.median(data=data)
1061        #=> None
1062        ```
1063        """
1064        if not isinstance(data, (list)):
1065            raise Exception("`median` `data` must be a list")
1066        length = len(data)
1067        if length == 0:
1068            return None
1069        data = sorted(data)
1070        if length % 2 == 0:
1071            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1072        return data[int(length / 2)]
1073
1074    def mergeDeep(self, update_data, data):
1075        """
1076        Function:
1077
1078        - Recursively merges two nested dictionaries keeping all keys at each layer
1079        - Values from `update_data` are used when keys are present in both dictionaries
1080
1081        Requires:
1082
1083        - `update_data`:
1084            - Type: any
1085            - What: The new data that will take precedence during merging
1086        - `data`:
1087            - Type: any
1088            - What: The original data that will be merged into
1089
1090        Example:
1091
1092        ```
1093        data={'a':{'b':{'c':'d'},'e':'f'}}
1094        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1095        pamda.mergeDeep(
1096            update_data=update_data,
1097            data=data
1098        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1099        ```
1100        """
1101        return __mergeDeep__(update_data, data)
1102
1103    def nest(self, path_keys: list, value_key: str, data: list):
1104        """
1105        Function:
1106
1107        - Nests a list of dictionaries into a nested dictionary
1108        - Similar items are appended to a list in the end of the nested dictionary
1109
1110        Requires:
1111
1112        - `path_keys`:
1113            - Type: list of strs
1114            - What: The variables to pull from each item in data
1115            - Note: Used to build out the nested dicitonary
1116            - Note: Order matters as the nesting occurs in order of variable
1117        - `value_key`:
1118            - Type: str
1119            - What: The variable to add to the list at the end of the nested dictionary path
1120        - `data`:
1121            - Type: list of dicts
1122            - What: A list of dictionaries to use for nesting purposes
1123
1124        Example:
1125
1126        ```
1127        data=[
1128            {'x_1':'a','x_2':'b', 'output':'c'},
1129            {'x_1':'a','x_2':'b', 'output':'d'},
1130            {'x_1':'a','x_2':'e', 'output':'f'}
1131        ]
1132        pamda.nest(
1133            path_keys=['x_1','x_2'],
1134            value_key='output',
1135            data=data
1136        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1137        ```
1138        """
1139        if not isinstance(data, list):
1140            raise Exception("Attempting to `nest` an object that is not a list")
1141        if len(data) == 0:
1142            raise Exception("Attempting to `nest` from an empty list")
1143        nested_output = {}
1144        for item in self.groupKeys(keys=path_keys, data=data):
1145            nested_output = __assocPath__(
1146                path=__getKeyValues__(path_keys, item[0]),
1147                value=[i.get(value_key) for i in item],
1148                data=nested_output,
1149            )
1150        return nested_output
1151
1152    def nestItem(self, path_keys: list, data: list):
1153        """
1154        Function:
1155
1156        - Nests a list of dictionaries into a nested dictionary
1157        - Similar items are appended to a list in the end of the nested dictionary
1158        - Similar to `nest`, except no values are plucked for the aggregated list
1159
1160        Requires:
1161
1162        - `path_keys`:
1163            - Type: list of strs
1164            - What: The variables to pull from each item in data
1165            - Note: Used to build out the nested dicitonary
1166            - Note: Order matters as the nesting occurs in order of variable
1167        - `data`:
1168            - Type: list of dicts
1169            - What: A list of dictionaries to use for nesting purposes
1170
1171        Example:
1172
1173        ```
1174        data=[
1175            {'x_1':'a','x_2':'b'},
1176            {'x_1':'a','x_2':'b'},
1177            {'x_1':'a','x_2':'e'}
1178        ]
1179        pamda.nestItem
1180            path_keys=['x_1','x_2'],
1181            data=data
1182        )
1183        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1184
1185        ```
1186        """
1187        if not isinstance(data, list):
1188            raise Exception("Attempting to `nest` an object that is not a list")
1189        if len(data) == 0:
1190            raise Exception("Attempting to `nest` from an empty list")
1191        nested_output = {}
1192        for item in self.groupKeys(keys=path_keys, data=data):
1193            nested_output = __assocPath__(
1194                path=__getKeyValues__(path_keys, item[0]),
1195                value=item,
1196                data=nested_output,
1197            )
1198        return nested_output
1199
1200    def path(self, path: list | str, data: dict):
1201        """
1202        Function:
1203
1204        - Returns the value of a path within a nested dictionary or None if the path does not exist
1205
1206        Requires:
1207
1208        - `path`:
1209            - Type: list of strs | str
1210            - What: The path to pull given the data
1211            - Note: If a string is passed, assumes a single item path list with that string
1212        - `data`:
1213            - Type: dict
1214            - What: A dictionary to get the path from
1215
1216        Example:
1217
1218        ```
1219        data={'a':{'b':1}}
1220        pamda.path(path=['a','b'], data=data) #=> 1
1221        ```
1222        """
1223        if isinstance(path, str):
1224            path = [path]
1225        return __pathOr__(None, path, data)
1226
1227    def pathOr(self, default, path: list | str, data: dict):
1228        """
1229        Function:
1230
1231        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1232
1233        Requires:
1234
1235        - `default`:
1236            - Type: any
1237            - What: The object to return if the path does not exist
1238        - `path`:
1239            - Type: list of strs | str
1240            - What: The path to pull given the data
1241            - Note: If a string is passed, assumes a single item path list with that string
1242        - `data`:
1243            - Type: dict
1244            - What: A dictionary to get the path from
1245
1246        Example:
1247
1248        ```
1249        data={'a':{'b':1}}
1250        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1251        ```
1252        """
1253        if isinstance(path, str):
1254            path = [path]
1255        return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get(
1256            path[-1], default
1257        )
1258
1259    def pipe(self, fns: list, args: tuple, kwargs: dict):
1260        """
1261        Function:
1262
1263        - Pipes data through n functions in order (left to right composition) and returns the output
1264
1265        Requires:
1266
1267        - `fns`:
1268            - Type: list of (functions | methods)
1269            - What: The list of functions and methods to pipe the data through
1270            - Notes: The first function in the list can be any arity (accepting any number of inputs)
1271            - Notes: Any further function in the list can only be unary (single input)
1272            - Notes: A function can be curried, but is not required to be
1273            - Notes: You may opt to curry functions and add inputs to make them unary
1274        - `args`:
1275            - Type: tuple
1276            - What: a tuple of positional arguments to pass to the first function in `fns`
1277        - `kwargs`:
1278            - Type: dict
1279            - What: a dictionary of keyword arguments to pass to the first function in `fns`
1280
1281        Examples:
1282
1283        ```
1284        data=['abc','def']
1285        pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
1286        pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
1287        ```
1288
1289        ```
1290        data={'a':{'b':'c'}}
1291        curriedPath=pamda.curry(pamda.path)
1292        pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1293        ```
1294        """
1295        if len(fns) == 0:
1296            raise Exception("`fns` must be a list with at least one function")
1297        if self.getArity(fns[0]) == 0:
1298            raise Exception(
1299                "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0."
1300            )
1301        if not all([(self.getArity(fn) == 1) for fn in fns[1:]]):
1302            raise Exception(
1303                "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)."
1304            )
1305        out = fns[0](*args, **kwargs)
1306        for fn in fns[1:]:
1307            out = fn(out)
1308        return out
1309
1310    def pivot(self, data: list[dict] | dict[Any, list]):
1311        """
1312        Function:
1313
1314        - Pivots a list of dictionaries into a dictionary of lists
1315        - Pivots a dictionary of lists into a list of dictionaries
1316
1317        Requires:
1318
1319        - `data`:
1320            - Type: list of dicts | dict of lists
1321            - What: The data to pivot
1322            - Note: If a list of dictionaries is passed, all dictionaries must have the same keys
1323            - Note: If a dictionary of lists is passed, all lists must have the same length
1324
1325        Example:
1326
1327        ```
1328        data=[
1329            {'a':1,'b':2},
1330            {'a':3,'b':4}
1331        ]
1332        pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
1333
1334        data={'a':[1,3],'b':[2,4]}
1335        pamda.pivot(data=data)
1336        #=> [
1337        #=>     {'a':1,'b':2},
1338        #=>     {'a':3,'b':4}
1339        #=> ]
1340        ```
1341        """
1342        if isinstance(data, list):
1343            return {
1344                key: [record[key] for record in data] for key in data[0].keys()
1345            }
1346        else:
1347            return [
1348                {key: data[key][i] for key in data.keys()}
1349                for i in range(len(data[list(data.keys())[0]]))
1350            ]
1351
1352    def pluck(self, path: list | str, data: list):
1353        """
1354        Function:
1355
1356        - Returns the values of a path within a list of nested dictionaries
1357
1358        Requires:
1359
1360        - `path`:
1361            - Type: list of strs
1362            - What: The path to pull given the data
1363            - Note: If a string is passed, assumes a single item path list with that string
1364        - `data`:
1365            - Type: list of dicts
1366            - What: A list of dictionaries to get the path from
1367
1368        Example:
1369
1370        ```
1371        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1372        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1373        ```
1374        """
1375        if len(data) == 0:
1376            raise Exception("Attempting to pluck from an empty list")
1377        if isinstance(path, str):
1378            path = [path]
1379        return [__pathOr__(default=None, path=path, data=i) for i in data]
1380
1381    def pluckIf(self, fn, path: list | str, data: list):
1382        """
1383        Function:
1384
1385        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1386
1387        Requires:
1388
1389        - `fn`:
1390            - Type: function
1391            - What: A function to take in each item in data and return a boolean
1392            - Note: Only items that return true are plucked
1393            - Note: Should be a unary function (take one input)
1394        - `path`:
1395            - Type: list of strs
1396            - What: The path to pull given the data
1397            - Note: If a string is passed, assumes a single item path list with that string
1398        - `data`:
1399            - Type: list of dicts
1400            - What: A list of dictionary to get the path from
1401
1402        Example:
1403
1404        ```
1405
1406        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1407        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1408        ```
1409        """
1410        if len(data) == 0:
1411            raise Exception("Attempting to pluck from an empty list")
1412        curried_fn = self.curry(fn)
1413        if curried_fn.__arity__ != 1:
1414            raise Exception(
1415                "`pluckIf` `fn` must have an arity of 1 (take one input)"
1416            )
1417        if isinstance(path, str):
1418            path = [path]
1419        return [
1420            __pathOr__(default=None, path=path, data=i) for i in data if fn(i)
1421        ]
1422
1423    def project(self, keys: list[str], data: list[dict]):
1424        """
1425        Function:
1426
1427        - Returns a list of dictionaries with only the keys provided
1428        - Analogous to SQL's `SELECT` statement
1429
1430        Requires:
1431
1432        - `keys`:
1433            - Type: list of strs
1434            - What: The keys to select from each dictionary in the data list
1435        - `data`:
1436            - Type: list of dicts
1437            - What: The list of dictionaries to select from
1438
1439        Example:
1440
1441        ```
1442        data=[
1443            {'a':1,'b':2,'c':3},
1444            {'a':4,'b':5,'c':6}
1445        ]
1446        pamda.project(keys=['a','c'], data=data)
1447        #=> [
1448        #=>     {'a':1,'c':3},
1449        #=>     {'a':4,'c':6}
1450        #=> ]
1451        ```
1452        """
1453        return [{key: record[key] for key in keys} for record in data]
1454
1455    def props(self, keys: list[str], data: dict):
1456        """
1457        Function:
1458
1459        - Returns the values of a list of keys within a dictionary
1460
1461        Requires:
1462
1463        - `keys`:
1464            - Type: list of strs
1465            - What: The keys to pull given the data
1466        - `data`:
1467            - Type: dict
1468            - What: A dictionary to get the keys from
1469
1470        Example:
1471        ```
1472        data={'a':1,'b':2,'c':3}
1473        pamda.props(keys=['a','c'], data=data)
1474        #=> [1,3]
1475        ```
1476        """
1477        return [data[key] for key in keys]
1478
1479    def reduce(self, fn, initial_accumulator, data: list):
1480        """
1481        Function:
1482
1483        - Returns a single item by iterating a function starting with an accumulator over a list
1484
1485        Requires:
1486
1487        - `fn`:
1488            - Type: function | method
1489            - What: The function or method to reduce
1490            - Note: This function should have an arity of 2 (take two inputs)
1491            - Note: The first input should take the accumulator value
1492            - Note: The second input should take the data value
1493        -`initial_accumulator`:
1494            - Type: any
1495            - What: The initial item to pass into the function when starting the accumulation process
1496        - `data`:
1497            - Type: list
1498            - What: The list of items to iterate over
1499
1500        Example:
1501
1502        ```
1503        data=[1,2,3,4]
1504        pamda.reduce(
1505            fn=pamda.add,
1506            initial_accumulator=0,
1507            data=data
1508        )
1509        #=> 10
1510
1511        ```
1512        """
1513        fn = self.curry(fn)
1514        if fn.__arity__ != 2:
1515            raise Exception(
1516                "`reduce` `fn` must have an arity of 2 (take two inputs)"
1517            )
1518        if not isinstance(data, (list)):
1519            raise Exception("`reduce` `data` must be a list")
1520        if not len(data) > 0:
1521            raise Exception(
1522                "`reduce` `data` has a length of 0, however it must have a length of at least 1"
1523            )
1524        acc = initial_accumulator
1525        for i in data:
1526            acc = fn(acc, i)
1527        return acc
1528
1529    def safeDivide(self, denominator: int | float, a: int | float):
1530        """
1531        Function:
1532
1533        - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
1534
1535        Requires:
1536
1537        - `denominator`:
1538            - Type: int | float
1539            - What: The denominator
1540
1541        - `a`:
1542            - Type: int | float
1543            - What: The numerator
1544
1545        Example:
1546
1547        ```
1548        pamda.safeDivide(2,10) #=> 5
1549        pamda.safeDivide(0,10) #=> 10
1550        ```
1551        """
1552        return a / denominator if denominator != 0 else a
1553
1554    def safeDivideDefault(
1555        self,
1556        default_denominator: int | float,
1557        denominator: int | float,
1558        a: int | float,
1559    ):
1560        """
1561        Function:
1562
1563        - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
1564
1565        Requires:
1566
1567        - `default_denominator`:
1568            - Type: int | float
1569            - What: A non zero denominator to use if denominator is zero
1570            - Default: 1
1571        - `denominator`:
1572            - Type: int | float
1573            - What: The denominator
1574        - `a`:
1575            - Type: int | float
1576            - What: The numerator
1577
1578        Example:
1579
1580        ```
1581        pamda.safeDivideDefault(2,5,10) #=> 2
1582        pamda.safeDivideDefault(2,0,10) #=> 5
1583        ```
1584        """
1585        if default_denominator == 0:
1586            raise Exception(
1587                "`safeDivideDefault` `default_denominator` can not be 0"
1588            )
1589        return a / denominator if denominator != 0 else a / default_denominator
1590
1591    def symmetricDifference(self, a: list, b: list):
1592        """
1593        Function:
1594
1595        - Combines two lists into a list of no duplicates items present in one list but not the other
1596
1597        Requires:
1598
1599        - `a`:
1600            - Type: list
1601            - What: List of items in which to look for a difference
1602        - `b`:
1603            - Type: list
1604            - What: List of items in which to look for a difference
1605
1606        Example:
1607
1608        ```
1609        a=['a','b']
1610        b=['b','c']
1611        pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1612        ```
1613        """
1614        return list(set(a).difference(set(b))) + list(set(b).difference(set(a)))
1615
1616    def tail(self, data: list | str):
1617        """
1618        Function:
1619
1620        - Picks the last item out of a list or string
1621
1622        Requires:
1623
1624        - `data`:
1625            - Type: list | str
1626            - What: A list or string
1627
1628        Example:
1629
1630        ```
1631        data=['fe','fi','fo','fum']
1632        pamda.tail(
1633            data=data
1634        ) #=> fum
1635        ```
1636        """
1637        if not len(data) > 0:
1638            raise Exception("Attempting to call `tail` on an empty list or str")
1639        return data[-1]
1640
1641    def thunkify(self, fn):
1642        """
1643        Function:
1644
1645        - Creates a curried thunk out of a function
1646        - Evaluation of the thunk lazy and is delayed until called
1647
1648        Requires:
1649
1650        - `fn`:
1651            - Type: function | method
1652            - What: The function or method to thunkify
1653            - Note: Thunkified functions are automatically curried
1654            - Note: Class methods auto apply self during thunkify
1655
1656        Notes:
1657
1658        - Input functions are not thunkified in place
1659        - The returned function is a thunkified version of the input function
1660        - A curried function can be thunkified in place by calling fn.thunkify()
1661
1662        Examples:
1663
1664        ```
1665        def add(a,b):
1666            return a+b
1667
1668        addThunk=pamda.thunkify(add)
1669
1670        add(1,2) #=> 3
1671        addThunk(1,2)
1672        addThunk(1,2)() #=> 3
1673
1674        x=addThunk(1,2)
1675        x() #=> 3
1676        ```
1677
1678        ```
1679        @pamda.curry
1680        def add(a,b):
1681            return a+b
1682
1683        add(1,2) #=> 3
1684
1685        add.thunkify()
1686
1687        add(1,2)
1688        add(1,2)() #=> 3
1689        ```
1690        """
1691        fn = self.curry(fn)
1692        return fn.thunkify()
1693
1694    def unnest(self, data: list):
1695        """
1696        Function:
1697
1698        - Removes one level of depth for all items in a list
1699
1700        Requires:
1701
1702        - `data`:
1703            - Type: list
1704            - What: A list of items to unnest by one level
1705
1706        Examples:
1707
1708        ```
1709        data=['fe','fi',['fo',['fum']]]
1710        pamda.unnest(
1711            data=data
1712        ) #=> ['fe','fi','fo',['fum']]
1713        ```
1714        """
1715        if not len(data) > 0:
1716            raise Exception("Attempting to call `unnest` on an empty list")
1717        output = []
1718        for i in data:
1719            if isinstance(i, list):
1720                output += i
1721            else:
1722                output.append(i)
1723        return output
1724
1725    def zip(self, a: list, b: list):
1726        """
1727        Function:
1728
1729        - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
1730
1731        Requires:
1732
1733        - `a`:
1734            - Type: list
1735            - What: List of items to appear in new list first
1736        - `b`:
1737            - Type: list
1738            - What: List of items to appear in new list second
1739
1740        Example:
1741
1742        ```
1743        a=['a','b']
1744        b=[1,2]
1745        pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1746        ```
1747        """
1748        return list(map(list, zip(a, b)))
1749
1750    def zipObj(self, a: list, b: list):
1751        """
1752        Function:
1753
1754        - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
1755        - The first list represents keys and the second values
1756
1757        Requires:
1758
1759        - `a`:
1760            - Type: list
1761            - What: List of items to appear in new list first
1762        - `b`:
1763            - Type: list
1764            - What: List of items to appear in new list second
1765
1766        Example:
1767
1768        ```
1769        a=['a','b']
1770        b=[1,2]
1771        pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}
1772        ```
1773        """
1774        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        return path[-1] in reduce(lambda x, y: x.get(y, {}), path[:-1], data)

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):
848    def hardRound(self, decimal_places: int, a: int | float):
849        """
850        Function:
851
852        - Rounds to a set number of decimal places regardless of floating point math in python
853
854        Requires:
855
856        - `decimal_places`:
857            - Type: int
858            - What: The number of decimal places to round to
859            - Default: 0
860            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
861        - `a`:
862            - Type: int | float
863            - What: The number to round
864
865        Example:
866
867        ```
868        a=12.345
869        pamda.hardRound(1,a) #=> 12.3
870        pamda.hardRound(-1,a) #=> 10
871        ```
872        """
873        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):
875    def head(self, data: list | str):
876        """
877        Function:
878
879        - Picks the first item out of a list or string
880
881        Requires:
882
883        - `data`:
884            - Type: list | str
885            - What: A list or string
886
887        Example:
888
889        ```
890        data=['fe','fi','fo','fum']
891        pamda.first(
892            data=data
893        ) #=> fe
894        ```
895        """
896        if not isinstance(data, (list, str)):
897            raise Exception("`head` can only be called on a `str` or a `list`")
898        if not len(data) > 0:
899            raise Exception("Attempting to call `head` on an empty list or str")
900        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):
902    def inc(self, a: int | float):
903        """
904        Function:
905
906        - Increments a number by one
907
908        Requires:
909
910        - `a`:
911            - Type: int | float
912            - What: The number to increment
913
914        Example:
915
916        ```
917        pamda.inc(42) #=> 43
918        ```
919        """
920        if not isinstance(a, (int, float)):
921            raise Exception("`a` must be an `int` or a `float`")
922        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):
924    def intersection(self, a: list, b: list):
925        """
926        Function:
927
928        - Combines two lists into a list of no duplicates composed of those elements common to both lists
929
930        Requires:
931
932        - `a`:
933            - Type: list
934            - What: List of items in which to look for an intersection
935        - `b`:
936            - Type: list
937            - What: List of items in which to look for an intersection
938
939        Example:
940
941        ```
942        a=['a','b']
943        b=['b','c']
944        pamda.intersection(a=a, b=b) #=> ['b']
945        ```
946        """
947        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):
949    def map(self, fn, data: list | dict):
950        """
951        Function:
952
953        - Maps a function over a list or a dictionary
954
955        Requires:
956
957        - `fn`:
958            - Type: function | method
959            - What: The function or method to map over the list or dictionary
960            - Note: This function should have an arity of 1
961        - `data`:
962            - Type: list | dict
963            - What: The list or dict of items to map the function over
964
965        Examples:
966
967        ```
968        data=[1,2,3]
969        pamda.map(
970            fn=pamda.inc,
971            data=data
972        )
973        #=> [2,3,4]
974        ```
975
976        ```
977        data={'a':1,'b':2,'c':3}
978        pamda.map(
979            fn=pamda.inc,
980            data=data
981        )
982        #=> {'a':2,'b':3,'c':4}
983        ```
984
985        """
986        # TODO: Check for efficiency gains
987        fn = self.curry(fn)
988        if fn.__arity__ != 1:
989            raise Exception("`map` `fn` must be unary (take one input)")
990        if not len(data) > 0:
991            raise Exception(
992                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
993            )
994        if isinstance(data, dict):
995            return {key: fn(value) for key, value in data.items()}
996        else:
997            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):
 999    def mean(self, data: list):
1000        """
1001        Function:
1002
1003        - Calculates the mean of a given list
1004
1005        Requires:
1006
1007        - `data`:
1008            - Type: list of (floats | ints)
1009            - What: The list with wich to calculate the mean
1010            - Note: If the length of this list is 0, returns None
1011
1012        Example:
1013
1014        ```
1015        data=[1,2,3]
1016        pamda.mean(data=data)
1017        #=> 2
1018        ```
1019
1020        ```
1021        data=[]
1022        pamda.mean(data=data)
1023        #=> None
1024        ```
1025        """
1026        if len(data) == 0:
1027            return None
1028        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):
1030    def median(self, data: list):
1031        """
1032        Function:
1033
1034        - Calculates the median of a given list
1035        - If the length of the list is even, calculates the mean of the two central values
1036
1037        Requires:
1038
1039        - `data`:
1040            - Type: list of (floats | ints)
1041            - What: The list with wich to calculate the mean
1042            - Note: If the length of this list is 0, returns None
1043
1044        Examples:
1045
1046        ```
1047        data=[7,2,8,9]
1048        pamda.median(data=data)
1049        #=> 7.5
1050        ```
1051
1052        ```
1053        data=[7,8,9]
1054        pamda.median(data=data)
1055        #=> 8
1056        ```
1057
1058        ```
1059        data=[]
1060        pamda.median(data=data)
1061        #=> None
1062        ```
1063        """
1064        if not isinstance(data, (list)):
1065            raise Exception("`median` `data` must be a list")
1066        length = len(data)
1067        if length == 0:
1068            return None
1069        data = sorted(data)
1070        if length % 2 == 0:
1071            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1072        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):
1074    def mergeDeep(self, update_data, data):
1075        """
1076        Function:
1077
1078        - Recursively merges two nested dictionaries keeping all keys at each layer
1079        - Values from `update_data` are used when keys are present in both dictionaries
1080
1081        Requires:
1082
1083        - `update_data`:
1084            - Type: any
1085            - What: The new data that will take precedence during merging
1086        - `data`:
1087            - Type: any
1088            - What: The original data that will be merged into
1089
1090        Example:
1091
1092        ```
1093        data={'a':{'b':{'c':'d'},'e':'f'}}
1094        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1095        pamda.mergeDeep(
1096            update_data=update_data,
1097            data=data
1098        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1099        ```
1100        """
1101        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):
1103    def nest(self, path_keys: list, value_key: str, data: list):
1104        """
1105        Function:
1106
1107        - Nests a list of dictionaries into a nested dictionary
1108        - Similar items are appended to a list in the end of the nested dictionary
1109
1110        Requires:
1111
1112        - `path_keys`:
1113            - Type: list of strs
1114            - What: The variables to pull from each item in data
1115            - Note: Used to build out the nested dicitonary
1116            - Note: Order matters as the nesting occurs in order of variable
1117        - `value_key`:
1118            - Type: str
1119            - What: The variable to add to the list at the end of the nested dictionary path
1120        - `data`:
1121            - Type: list of dicts
1122            - What: A list of dictionaries to use for nesting purposes
1123
1124        Example:
1125
1126        ```
1127        data=[
1128            {'x_1':'a','x_2':'b', 'output':'c'},
1129            {'x_1':'a','x_2':'b', 'output':'d'},
1130            {'x_1':'a','x_2':'e', 'output':'f'}
1131        ]
1132        pamda.nest(
1133            path_keys=['x_1','x_2'],
1134            value_key='output',
1135            data=data
1136        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1137        ```
1138        """
1139        if not isinstance(data, list):
1140            raise Exception("Attempting to `nest` an object that is not a list")
1141        if len(data) == 0:
1142            raise Exception("Attempting to `nest` from an empty list")
1143        nested_output = {}
1144        for item in self.groupKeys(keys=path_keys, data=data):
1145            nested_output = __assocPath__(
1146                path=__getKeyValues__(path_keys, item[0]),
1147                value=[i.get(value_key) for i in item],
1148                data=nested_output,
1149            )
1150        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):
1152    def nestItem(self, path_keys: list, data: list):
1153        """
1154        Function:
1155
1156        - Nests a list of dictionaries into a nested dictionary
1157        - Similar items are appended to a list in the end of the nested dictionary
1158        - Similar to `nest`, except no values are plucked for the aggregated list
1159
1160        Requires:
1161
1162        - `path_keys`:
1163            - Type: list of strs
1164            - What: The variables to pull from each item in data
1165            - Note: Used to build out the nested dicitonary
1166            - Note: Order matters as the nesting occurs in order of variable
1167        - `data`:
1168            - Type: list of dicts
1169            - What: A list of dictionaries to use for nesting purposes
1170
1171        Example:
1172
1173        ```
1174        data=[
1175            {'x_1':'a','x_2':'b'},
1176            {'x_1':'a','x_2':'b'},
1177            {'x_1':'a','x_2':'e'}
1178        ]
1179        pamda.nestItem
1180            path_keys=['x_1','x_2'],
1181            data=data
1182        )
1183        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1184
1185        ```
1186        """
1187        if not isinstance(data, list):
1188            raise Exception("Attempting to `nest` an object that is not a list")
1189        if len(data) == 0:
1190            raise Exception("Attempting to `nest` from an empty list")
1191        nested_output = {}
1192        for item in self.groupKeys(keys=path_keys, data=data):
1193            nested_output = __assocPath__(
1194                path=__getKeyValues__(path_keys, item[0]),
1195                value=item,
1196                data=nested_output,
1197            )
1198        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):
1200    def path(self, path: list | str, data: dict):
1201        """
1202        Function:
1203
1204        - Returns the value of a path within a nested dictionary or None if the path does not exist
1205
1206        Requires:
1207
1208        - `path`:
1209            - Type: list of strs | str
1210            - What: The path to pull given the data
1211            - Note: If a string is passed, assumes a single item path list with that string
1212        - `data`:
1213            - Type: dict
1214            - What: A dictionary to get the path from
1215
1216        Example:
1217
1218        ```
1219        data={'a':{'b':1}}
1220        pamda.path(path=['a','b'], data=data) #=> 1
1221        ```
1222        """
1223        if isinstance(path, str):
1224            path = [path]
1225        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):
1227    def pathOr(self, default, path: list | str, data: dict):
1228        """
1229        Function:
1230
1231        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1232
1233        Requires:
1234
1235        - `default`:
1236            - Type: any
1237            - What: The object to return if the path does not exist
1238        - `path`:
1239            - Type: list of strs | str
1240            - What: The path to pull given the data
1241            - Note: If a string is passed, assumes a single item path list with that string
1242        - `data`:
1243            - Type: dict
1244            - What: A dictionary to get the path from
1245
1246        Example:
1247
1248        ```
1249        data={'a':{'b':1}}
1250        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1251        ```
1252        """
1253        if isinstance(path, str):
1254            path = [path]
1255        return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get(
1256            path[-1], default
1257        )

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):
1259    def pipe(self, fns: list, args: tuple, kwargs: dict):
1260        """
1261        Function:
1262
1263        - Pipes data through n functions in order (left to right composition) and returns the output
1264
1265        Requires:
1266
1267        - `fns`:
1268            - Type: list of (functions | methods)
1269            - What: The list of functions and methods to pipe the data through
1270            - Notes: The first function in the list can be any arity (accepting any number of inputs)
1271            - Notes: Any further function in the list can only be unary (single input)
1272            - Notes: A function can be curried, but is not required to be
1273            - Notes: You may opt to curry functions and add inputs to make them unary
1274        - `args`:
1275            - Type: tuple
1276            - What: a tuple of positional arguments to pass to the first function in `fns`
1277        - `kwargs`:
1278            - Type: dict
1279            - What: a dictionary of keyword arguments to pass to the first function in `fns`
1280
1281        Examples:
1282
1283        ```
1284        data=['abc','def']
1285        pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
1286        pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
1287        ```
1288
1289        ```
1290        data={'a':{'b':'c'}}
1291        curriedPath=pamda.curry(pamda.path)
1292        pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1293        ```
1294        """
1295        if len(fns) == 0:
1296            raise Exception("`fns` must be a list with at least one function")
1297        if self.getArity(fns[0]) == 0:
1298            raise Exception(
1299                "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0."
1300            )
1301        if not all([(self.getArity(fn) == 1) for fn in fns[1:]]):
1302            raise Exception(
1303                "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)."
1304            )
1305        out = fns[0](*args, **kwargs)
1306        for fn in fns[1:]:
1307            out = fn(out)
1308        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]):
1310    def pivot(self, data: list[dict] | dict[Any, list]):
1311        """
1312        Function:
1313
1314        - Pivots a list of dictionaries into a dictionary of lists
1315        - Pivots a dictionary of lists into a list of dictionaries
1316
1317        Requires:
1318
1319        - `data`:
1320            - Type: list of dicts | dict of lists
1321            - What: The data to pivot
1322            - Note: If a list of dictionaries is passed, all dictionaries must have the same keys
1323            - Note: If a dictionary of lists is passed, all lists must have the same length
1324
1325        Example:
1326
1327        ```
1328        data=[
1329            {'a':1,'b':2},
1330            {'a':3,'b':4}
1331        ]
1332        pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
1333
1334        data={'a':[1,3],'b':[2,4]}
1335        pamda.pivot(data=data)
1336        #=> [
1337        #=>     {'a':1,'b':2},
1338        #=>     {'a':3,'b':4}
1339        #=> ]
1340        ```
1341        """
1342        if isinstance(data, list):
1343            return {
1344                key: [record[key] for record in data] for key in data[0].keys()
1345            }
1346        else:
1347            return [
1348                {key: data[key][i] for key in data.keys()}
1349                for i in range(len(data[list(data.keys())[0]]))
1350            ]

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):
1352    def pluck(self, path: list | str, data: list):
1353        """
1354        Function:
1355
1356        - Returns the values of a path within a list of nested dictionaries
1357
1358        Requires:
1359
1360        - `path`:
1361            - Type: list of strs
1362            - What: The path to pull given the data
1363            - Note: If a string is passed, assumes a single item path list with that string
1364        - `data`:
1365            - Type: list of dicts
1366            - What: A list of dictionaries to get the path from
1367
1368        Example:
1369
1370        ```
1371        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1372        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1373        ```
1374        """
1375        if len(data) == 0:
1376            raise Exception("Attempting to pluck from an empty list")
1377        if isinstance(path, str):
1378            path = [path]
1379        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):
1381    def pluckIf(self, fn, path: list | str, data: list):
1382        """
1383        Function:
1384
1385        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1386
1387        Requires:
1388
1389        - `fn`:
1390            - Type: function
1391            - What: A function to take in each item in data and return a boolean
1392            - Note: Only items that return true are plucked
1393            - Note: Should be a unary function (take one input)
1394        - `path`:
1395            - Type: list of strs
1396            - What: The path to pull given the data
1397            - Note: If a string is passed, assumes a single item path list with that string
1398        - `data`:
1399            - Type: list of dicts
1400            - What: A list of dictionary to get the path from
1401
1402        Example:
1403
1404        ```
1405
1406        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1407        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1408        ```
1409        """
1410        if len(data) == 0:
1411            raise Exception("Attempting to pluck from an empty list")
1412        curried_fn = self.curry(fn)
1413        if curried_fn.__arity__ != 1:
1414            raise Exception(
1415                "`pluckIf` `fn` must have an arity of 1 (take one input)"
1416            )
1417        if isinstance(path, str):
1418            path = [path]
1419        return [
1420            __pathOr__(default=None, path=path, data=i) for i in data if fn(i)
1421        ]

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]):
1423    def project(self, keys: list[str], data: list[dict]):
1424        """
1425        Function:
1426
1427        - Returns a list of dictionaries with only the keys provided
1428        - Analogous to SQL's `SELECT` statement
1429
1430        Requires:
1431
1432        - `keys`:
1433            - Type: list of strs
1434            - What: The keys to select from each dictionary in the data list
1435        - `data`:
1436            - Type: list of dicts
1437            - What: The list of dictionaries to select from
1438
1439        Example:
1440
1441        ```
1442        data=[
1443            {'a':1,'b':2,'c':3},
1444            {'a':4,'b':5,'c':6}
1445        ]
1446        pamda.project(keys=['a','c'], data=data)
1447        #=> [
1448        #=>     {'a':1,'c':3},
1449        #=>     {'a':4,'c':6}
1450        #=> ]
1451        ```
1452        """
1453        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):
1455    def props(self, keys: list[str], data: dict):
1456        """
1457        Function:
1458
1459        - Returns the values of a list of keys within a dictionary
1460
1461        Requires:
1462
1463        - `keys`:
1464            - Type: list of strs
1465            - What: The keys to pull given the data
1466        - `data`:
1467            - Type: dict
1468            - What: A dictionary to get the keys from
1469
1470        Example:
1471        ```
1472        data={'a':1,'b':2,'c':3}
1473        pamda.props(keys=['a','c'], data=data)
1474        #=> [1,3]
1475        ```
1476        """
1477        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):
1479    def reduce(self, fn, initial_accumulator, data: list):
1480        """
1481        Function:
1482
1483        - Returns a single item by iterating a function starting with an accumulator over a list
1484
1485        Requires:
1486
1487        - `fn`:
1488            - Type: function | method
1489            - What: The function or method to reduce
1490            - Note: This function should have an arity of 2 (take two inputs)
1491            - Note: The first input should take the accumulator value
1492            - Note: The second input should take the data value
1493        -`initial_accumulator`:
1494            - Type: any
1495            - What: The initial item to pass into the function when starting the accumulation process
1496        - `data`:
1497            - Type: list
1498            - What: The list of items to iterate over
1499
1500        Example:
1501
1502        ```
1503        data=[1,2,3,4]
1504        pamda.reduce(
1505            fn=pamda.add,
1506            initial_accumulator=0,
1507            data=data
1508        )
1509        #=> 10
1510
1511        ```
1512        """
1513        fn = self.curry(fn)
1514        if fn.__arity__ != 2:
1515            raise Exception(
1516                "`reduce` `fn` must have an arity of 2 (take two inputs)"
1517            )
1518        if not isinstance(data, (list)):
1519            raise Exception("`reduce` `data` must be a list")
1520        if not len(data) > 0:
1521            raise Exception(
1522                "`reduce` `data` has a length of 0, however it must have a length of at least 1"
1523            )
1524        acc = initial_accumulator
1525        for i in data:
1526            acc = fn(acc, i)
1527        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):
1529    def safeDivide(self, denominator: int | float, a: int | float):
1530        """
1531        Function:
1532
1533        - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
1534
1535        Requires:
1536
1537        - `denominator`:
1538            - Type: int | float
1539            - What: The denominator
1540
1541        - `a`:
1542            - Type: int | float
1543            - What: The numerator
1544
1545        Example:
1546
1547        ```
1548        pamda.safeDivide(2,10) #=> 5
1549        pamda.safeDivide(0,10) #=> 10
1550        ```
1551        """
1552        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):
1554    def safeDivideDefault(
1555        self,
1556        default_denominator: int | float,
1557        denominator: int | float,
1558        a: int | float,
1559    ):
1560        """
1561        Function:
1562
1563        - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
1564
1565        Requires:
1566
1567        - `default_denominator`:
1568            - Type: int | float
1569            - What: A non zero denominator to use if denominator is zero
1570            - Default: 1
1571        - `denominator`:
1572            - Type: int | float
1573            - What: The denominator
1574        - `a`:
1575            - Type: int | float
1576            - What: The numerator
1577
1578        Example:
1579
1580        ```
1581        pamda.safeDivideDefault(2,5,10) #=> 2
1582        pamda.safeDivideDefault(2,0,10) #=> 5
1583        ```
1584        """
1585        if default_denominator == 0:
1586            raise Exception(
1587                "`safeDivideDefault` `default_denominator` can not be 0"
1588            )
1589        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):
1591    def symmetricDifference(self, a: list, b: list):
1592        """
1593        Function:
1594
1595        - Combines two lists into a list of no duplicates items present in one list but not the other
1596
1597        Requires:
1598
1599        - `a`:
1600            - Type: list
1601            - What: List of items in which to look for a difference
1602        - `b`:
1603            - Type: list
1604            - What: List of items in which to look for a difference
1605
1606        Example:
1607
1608        ```
1609        a=['a','b']
1610        b=['b','c']
1611        pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1612        ```
1613        """
1614        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):
1616    def tail(self, data: list | str):
1617        """
1618        Function:
1619
1620        - Picks the last item out of a list or string
1621
1622        Requires:
1623
1624        - `data`:
1625            - Type: list | str
1626            - What: A list or string
1627
1628        Example:
1629
1630        ```
1631        data=['fe','fi','fo','fum']
1632        pamda.tail(
1633            data=data
1634        ) #=> fum
1635        ```
1636        """
1637        if not len(data) > 0:
1638            raise Exception("Attempting to call `tail` on an empty list or str")
1639        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):
1641    def thunkify(self, fn):
1642        """
1643        Function:
1644
1645        - Creates a curried thunk out of a function
1646        - Evaluation of the thunk lazy and is delayed until called
1647
1648        Requires:
1649
1650        - `fn`:
1651            - Type: function | method
1652            - What: The function or method to thunkify
1653            - Note: Thunkified functions are automatically curried
1654            - Note: Class methods auto apply self during thunkify
1655
1656        Notes:
1657
1658        - Input functions are not thunkified in place
1659        - The returned function is a thunkified version of the input function
1660        - A curried function can be thunkified in place by calling fn.thunkify()
1661
1662        Examples:
1663
1664        ```
1665        def add(a,b):
1666            return a+b
1667
1668        addThunk=pamda.thunkify(add)
1669
1670        add(1,2) #=> 3
1671        addThunk(1,2)
1672        addThunk(1,2)() #=> 3
1673
1674        x=addThunk(1,2)
1675        x() #=> 3
1676        ```
1677
1678        ```
1679        @pamda.curry
1680        def add(a,b):
1681            return a+b
1682
1683        add(1,2) #=> 3
1684
1685        add.thunkify()
1686
1687        add(1,2)
1688        add(1,2)() #=> 3
1689        ```
1690        """
1691        fn = self.curry(fn)
1692        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):
1694    def unnest(self, data: list):
1695        """
1696        Function:
1697
1698        - Removes one level of depth for all items in a list
1699
1700        Requires:
1701
1702        - `data`:
1703            - Type: list
1704            - What: A list of items to unnest by one level
1705
1706        Examples:
1707
1708        ```
1709        data=['fe','fi',['fo',['fum']]]
1710        pamda.unnest(
1711            data=data
1712        ) #=> ['fe','fi','fo',['fum']]
1713        ```
1714        """
1715        if not len(data) > 0:
1716            raise Exception("Attempting to call `unnest` on an empty list")
1717        output = []
1718        for i in data:
1719            if isinstance(i, list):
1720                output += i
1721            else:
1722                output.append(i)
1723        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):
1725    def zip(self, a: list, b: list):
1726        """
1727        Function:
1728
1729        - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
1730
1731        Requires:
1732
1733        - `a`:
1734            - Type: list
1735            - What: List of items to appear in new list first
1736        - `b`:
1737            - Type: list
1738            - What: List of items to appear in new list second
1739
1740        Example:
1741
1742        ```
1743        a=['a','b']
1744        b=[1,2]
1745        pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1746        ```
1747        """
1748        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):
1750    def zipObj(self, a: list, b: list):
1751        """
1752        Function:
1753
1754        - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
1755        - The first list represents keys and the second values
1756
1757        Requires:
1758
1759        - `a`:
1760            - Type: list
1761            - What: List of items to appear in new list first
1762        - `b`:
1763            - Type: list
1764            - What: List of items to appear in new list second
1765
1766        Example:
1767
1768        ```
1769        a=['a','b']
1770        b=[1,2]
1771        pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}
1772        ```
1773        """
1774        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}