pamda.pamda

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

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
    • 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):
742    def groupKeys(self, keys: list, data: list):
743        """
744        Function:
745
746        - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys
747
748        Requires:
749
750        - `keys`:
751            - Type: list of strs
752            - What: The keys to group by
753        - `data`:
754            - Type: list of dicts
755            - What: List of dictionaries with which to match keys
756
757        Examples:
758
759        ```
760        data=[
761            {'color':'red', 'size':9, 'shape':'ball'},
762            {'color':'red', 'size':10, 'shape':'ball'},
763            {'color':'green', 'size':11, 'shape':'ball'},
764            {'color':'green', 'size':12, 'shape':'square'}
765        ]
766        pamda.groupKeys(['color','shape'],data)
767        #=> [
768        #=>     [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}],
769        #=>     [{'color': 'green', 'size': 11, 'shape': 'ball'}],
770        #=>     [{'color': 'green', 'size': 12, 'shape': 'square'}]
771        #=> ]
772        ```
773        """
774
775        def keyFn(item):
776            return str(([item[key] for key in keys]))
777
778        return list(self.groupBy(keyFn, 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):
780    def groupWith(self, fn, data: list):
781        """
782        Function:
783
784        - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function
785
786        Requires:
787
788        - `fn`:
789            - Type: function | method
790            - What: The function or method to groub with
791            - Note: Must return a boolean value
792            - Note: This function must have an arity of two (take two inputs)
793            - Note: This function is applied to each item plus the next adjacent item in the list recursively
794        - `data`:
795            - Type: list
796            - What: List of items to apply the function to and then group the results
797
798        Examples:
799
800        ```
801        def areEqual(a,b):
802            return a==b
803
804        data=[1,2,3,1,1,2,2,3,3,3]
805        pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]]
806        ```
807        """
808        curried_fn = self.curry(fn)
809        if curried_fn.__arity__ != 2:
810            raise Exception("groupWith `fn` must take exactly two parameters")
811        output = []
812        start = True
813        for i in data:
814            if start:
815                sublist = [i]
816                start = False
817            elif fn(i, previous):
818                sublist.append(i)
819            else:
820                output.append(sublist)
821                sublist = [i]
822            previous = i
823        output.append(sublist)
824        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):
826    def hasPath(self, path: list | str, data: dict):
827        """
828        Function:
829
830        - Checks if a path exists within a nested dictionary
831
832        Requires:
833
834        - `path`:
835            - Type: list of strs | str
836            - What: The path to check
837            - Note: If a string is passed, assumes a single item path list with that string
838        - `data`:
839            - Type: dict
840            - What: A dictionary to check if the path exists
841
842        Example:
843
844        ```
845        data={'a':{'b':1}}
846        pamda.hasPath(path=['a','b'], data=data) #=> True
847        pamda.hasPath(path=['a','d'], data=data) #=> False
848        ```
849        """
850        if isinstance(path, str):
851            path = [path]
852        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):
854    def hardRound(self, decimal_places: int, a: int | float):
855        """
856        Function:
857
858        - Rounds to a set number of decimal places regardless of floating point math in python
859
860        Requires:
861
862        - `decimal_places`:
863            - Type: int
864            - What: The number of decimal places to round to
865            - Default: 0
866            - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
867        - `a`:
868            - Type: int | float
869            - What: The number to round
870
871        Example:
872
873        ```
874        a=12.345
875        pamda.hardRound(1,a) #=> 12.3
876        pamda.hardRound(-1,a) #=> 10
877        ```
878        """
879        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):
881    def head(self, data: list | str):
882        """
883        Function:
884
885        - Picks the first item out of a list or string
886
887        Requires:
888
889        - `data`:
890            - Type: list | str
891            - What: A list or string
892
893        Example:
894
895        ```
896        data=['fe','fi','fo','fum']
897        pamda.first(
898            data=data
899        ) #=> fe
900        ```
901        """
902        if not isinstance(data, (list, str)):
903            raise Exception("`head` can only be called on a `str` or a `list`")
904        if not len(data) > 0:
905            raise Exception("Attempting to call `head` on an empty list or str")
906        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):
908    def inc(self, a: int | float):
909        """
910        Function:
911
912        - Increments a number by one
913
914        Requires:
915
916        - `a`:
917            - Type: int | float
918            - What: The number to increment
919
920        Example:
921
922        ```
923        pamda.inc(42) #=> 43
924        ```
925        """
926        if not isinstance(a, (int, float)):
927            raise Exception("`a` must be an `int` or a `float`")
928        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):
930    def intersection(self, a: list, b: list):
931        """
932        Function:
933
934        - Combines two lists into a list of no duplicates composed of those elements common to both lists
935
936        Requires:
937
938        - `a`:
939            - Type: list
940            - What: List of items in which to look for an intersection
941        - `b`:
942            - Type: list
943            - What: List of items in which to look for an intersection
944
945        Example:
946
947        ```
948        a=['a','b']
949        b=['b','c']
950        pamda.intersection(a=a, b=b) #=> ['b']
951        ```
952        """
953        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):
 955    def map(self, fn, data: list | dict):
 956        """
 957        Function:
 958
 959        - Maps a function over a list or a dictionary
 960
 961        Requires:
 962
 963        - `fn`:
 964            - Type: function | method
 965            - What: The function or method to map over the list or dictionary
 966            - Note: This function should have an arity of 1
 967        - `data`:
 968            - Type: list | dict
 969            - What: The list or dict of items to map the function over
 970
 971        Examples:
 972
 973        ```
 974        data=[1,2,3]
 975        pamda.map(
 976            fn=pamda.inc,
 977            data=data
 978        )
 979        #=> [2,3,4]
 980        ```
 981
 982        ```
 983        data={'a':1,'b':2,'c':3}
 984        pamda.map(
 985            fn=pamda.inc,
 986            data=data
 987        )
 988        #=> {'a':2,'b':3,'c':4}
 989        ```
 990
 991        """
 992        fn = self.curry(fn)
 993        if fn.__arity__ != 1:
 994            raise Exception("`map` `fn` must be unary (take one input)")
 995        if not len(data) > 0:
 996            raise Exception(
 997                "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it"
 998            )
 999        if isinstance(data, dict):
1000            return {key: fn(value) for key, value in data.items()}
1001        else:
1002            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):
1004    def mean(self, data: list):
1005        """
1006        Function:
1007
1008        - Calculates the mean of a given list
1009
1010        Requires:
1011
1012        - `data`:
1013            - Type: list of (floats | ints)
1014            - What: The list with wich to calculate the mean
1015            - Note: If the length of this list is 0, returns None
1016
1017        Example:
1018
1019        ```
1020        data=[1,2,3]
1021        pamda.mean(data=data)
1022        #=> 2
1023        ```
1024
1025        ```
1026        data=[]
1027        pamda.mean(data=data)
1028        #=> None
1029        ```
1030        """
1031        if len(data) == 0:
1032            return None
1033        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):
1035    def median(self, data: list):
1036        """
1037        Function:
1038
1039        - Calculates the median of a given list
1040        - If the length of the list is even, calculates the mean of the two central values
1041
1042        Requires:
1043
1044        - `data`:
1045            - Type: list of (floats | ints)
1046            - What: The list with wich to calculate the mean
1047            - Note: If the length of this list is 0, returns None
1048
1049        Examples:
1050
1051        ```
1052        data=[7,2,8,9]
1053        pamda.median(data=data)
1054        #=> 7.5
1055        ```
1056
1057        ```
1058        data=[7,8,9]
1059        pamda.median(data=data)
1060        #=> 8
1061        ```
1062
1063        ```
1064        data=[]
1065        pamda.median(data=data)
1066        #=> None
1067        ```
1068        """
1069        if not isinstance(data, (list)):
1070            raise Exception("`median` `data` must be a list")
1071        length = len(data)
1072        if length == 0:
1073            return None
1074        data = sorted(data)
1075        if length % 2 == 0:
1076            return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2
1077        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):
1079    def mergeDeep(self, update_data, data):
1080        """
1081        Function:
1082
1083        - Recursively merges two nested dictionaries keeping all keys at each layer
1084        - Values from `update_data` are used when keys are present in both dictionaries
1085
1086        Requires:
1087
1088        - `update_data`:
1089            - Type: any
1090            - What: The new data that will take precedence during merging
1091        - `data`:
1092            - Type: any
1093            - What: The original data that will be merged into
1094
1095        Example:
1096
1097        ```
1098        data={'a':{'b':{'c':'d'},'e':'f'}}
1099        update_data={'a':{'b':{'h':'i'},'e':'g'}}
1100        pamda.mergeDeep(
1101            update_data=update_data,
1102            data=data
1103        ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1104        ```
1105        """
1106        if not isinstance(data, dict) or not isinstance(update_data, dict):
1107            return update_data
1108        output = dict(data)
1109        keys_original = set(data.keys())
1110        keys_update = set(update_data.keys())
1111        similar_keys = keys_original.intersection(keys_update)
1112        similar_dict = {
1113            key: self.mergeDeep(update_data[key], data[key])
1114            for key in similar_keys
1115        }
1116        new_keys = keys_update.difference(keys_original)
1117        new_dict = {key: update_data[key] for key in new_keys}
1118        output.update(similar_dict)
1119        output.update(new_dict)
1120        return output

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):
1122    def nest(self, path_keys: list, value_key: str, data: list):
1123        """
1124        Function:
1125
1126        - Nests a list of dictionaries into a nested dictionary
1127        - Similar items are appended to a list in the end of the nested dictionary
1128
1129        Requires:
1130
1131        - `path_keys`:
1132            - Type: list of strs
1133            - What: The variables to pull from each item in data
1134            - Note: Used to build out the nested dicitonary
1135            - Note: Order matters as the nesting occurs in order of variable
1136        - `value_key`:
1137            - Type: str
1138            - What: The variable to add to the list at the end of the nested dictionary path
1139        - `data`:
1140            - Type: list of dicts
1141            - What: A list of dictionaries to use for nesting purposes
1142
1143        Example:
1144
1145        ```
1146        data=[
1147            {'x_1':'a','x_2':'b', 'output':'c'},
1148            {'x_1':'a','x_2':'b', 'output':'d'},
1149            {'x_1':'a','x_2':'e', 'output':'f'}
1150        ]
1151        pamda.nest(
1152            path_keys=['x_1','x_2'],
1153            value_key='output',
1154            data=data
1155        ) #=> {'a':{'b':['c','d'], 'e':['f']}}
1156        ```
1157        """
1158        if not isinstance(data, list):
1159            raise Exception("Attempting to `nest` an object that is not a list")
1160        if len(data) == 0:
1161            raise Exception("Attempting to `nest` from an empty list")
1162        nested_output = {}
1163        grouped_data = self.groupKeys(keys=path_keys, data=data)
1164        for item in grouped_data:
1165            nested_output = self.assocPath(
1166                path=[item[0].get(key) for key in path_keys],
1167                value=[i.get(value_key) for i in item],
1168                data=nested_output,
1169            )
1170        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):
1172    def nestItem(self, path_keys: list, data: list):
1173        """
1174        Function:
1175
1176        - Nests a list of dictionaries into a nested dictionary
1177        - Similar items are appended to a list in the end of the nested dictionary
1178        - Similar to `nest`, except no values are plucked for the aggregated list
1179
1180        Requires:
1181
1182        - `path_keys`:
1183            - Type: list of strs
1184            - What: The variables to pull from each item in data
1185            - Note: Used to build out the nested dicitonary
1186            - Note: Order matters as the nesting occurs in order of variable
1187        - `data`:
1188            - Type: list of dicts
1189            - What: A list of dictionaries to use for nesting purposes
1190
1191        Example:
1192
1193        ```
1194        data=[
1195            {'x_1':'a','x_2':'b'},
1196            {'x_1':'a','x_2':'b'},
1197            {'x_1':'a','x_2':'e'}
1198        ]
1199        pamda.nestItem
1200            path_keys=['x_1','x_2'],
1201            data=data
1202        )
1203        #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1204
1205        ```
1206        """
1207        if not isinstance(data, list):
1208            raise Exception("Attempting to `nest` an object that is not a list")
1209        if len(data) == 0:
1210            raise Exception("Attempting to `nest` from an empty list")
1211        nested_output = {}
1212        grouped_data = self.groupKeys(keys=path_keys, data=data)
1213        for item in grouped_data:
1214            nested_output = self.assocPath(
1215                path=[item[0].get(key) for key in path_keys],
1216                value=item,
1217                data=nested_output,
1218            )
1219        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):
1221    def path(self, path: list | str, data: dict):
1222        """
1223        Function:
1224
1225        - Returns the value of a path within a nested dictionary or None if the path does not exist
1226
1227        Requires:
1228
1229        - `path`:
1230            - Type: list of strs | str
1231            - What: The path to pull given the data
1232            - Note: If a string is passed, assumes a single item path list with that string
1233        - `data`:
1234            - Type: dict
1235            - What: A dictionary to get the path from
1236
1237        Example:
1238
1239        ```
1240        data={'a':{'b':1}}
1241        pamda.path(path=['a','b'], data=data) #=> 1
1242        ```
1243        """
1244        return self.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):
1246    def pathOr(self, default, path: list | str, data: dict):
1247        """
1248        Function:
1249
1250        - Returns the value of a path within a nested dictionary or a default value if that path does not exist
1251
1252        Requires:
1253
1254        - `default`:
1255            - Type: any
1256            - What: The object to return if the path does not exist
1257        - `path`:
1258            - Type: list of strs | str
1259            - What: The path to pull given the data
1260            - Note: If a string is passed, assumes a single item path list with that string
1261        - `data`:
1262            - Type: dict
1263            - What: A dictionary to get the path from
1264
1265        Example:
1266
1267        ```
1268        data={'a':{'b':1}}
1269        pamda.path(default=2, path=['a','c'], data=data) #=> 2
1270        ```
1271        """
1272        if isinstance(path, str):
1273            path = [path]
1274        return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get(
1275            path[-1], default
1276        )

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

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):
1371    def pluck(self, path: list | str, data: list):
1372        """
1373        Function:
1374
1375        - Returns the values of a path within a list of nested dictionaries
1376
1377        Requires:
1378
1379        - `path`:
1380            - Type: list of strs
1381            - What: The path to pull given the data
1382            - Note: If a string is passed, assumes a single item path list with that string
1383        - `data`:
1384            - Type: list of dicts
1385            - What: A list of dictionaries to get the path from
1386
1387        Example:
1388
1389        ```
1390        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1391        pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1392        ```
1393        """
1394        if len(data) == 0:
1395            raise Exception("Attempting to pluck from an empty list")
1396        return [self.path(data=i, path=path) 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):
1398    def pluckIf(self, fn, path: list | str, data: list):
1399        """
1400        Function:
1401
1402        - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
1403
1404        Requires:
1405
1406        - `fn`:
1407            - Type: function
1408            - What: A function to take in each item in data and return a boolean
1409            - Note: Only items that return true are plucked
1410        - `path`:
1411            - Type: list of strs
1412            - What: The path to pull given the data
1413            - Note: If a string is passed, assumes a single item path list with that string
1414        - `data`:
1415            - Type: list of dicts
1416            - What: A list of dictionary to get the path from
1417
1418        Example:
1419
1420        ```
1421
1422        data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
1423        pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1424        ```
1425        """
1426        if len(data) == 0:
1427            raise Exception("Attempting to pluck from an empty list")
1428        return [self.path(data=i, path=path) for i in data if fn(i)]

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