The above solution is great when the package references relevant functions/methods. But if they are not invoked by __init__, then they won't be listed.
After a bunch of digging I came up with the following solution. Probably not ideal, but worked for my use-case, so I'm re-sharing it here for others to use/improve upon.
def get_functions_and_methods(path):
    """
    Given a .py file path - returns a list with all functions and methods in it.
    Source: https://stackoverflow.com/q/73239026/256662
    """
    import ast
    with open(path) as file:
        node = ast.parse(file.read())
    def show_info(functionNode):
        function_rep = ''
        function_rep = functionNode.name + '('
        for arg in functionNode.args.args:
            function_rep += arg.arg + ','
        function_rep = function_rep.rstrip(function_rep[-1])
        function_rep += ')'
        return function_rep
    result = []
    functions = [n for n in node.body if isinstance(n, ast.FunctionDef)]
    classes = [n for n in node.body if isinstance(n, ast.ClassDef)]
    for function in functions:
        result.append(show_info(function))
    for class_ in classes:
        methods = [n for n in class_.body if isinstance(n, ast.FunctionDef)]
        for method in methods:
            result.append((class_.name + '.' + show_info(method)))
    # print(', '.join(result))
    return result
    # This prints expected output
    # fo(x), A.fun(self,y), A._bo(self,y), A.NS(y,z), B.foo(self,z), B._bar(self,t)
# Get paste to work (but it doesn't recycle :( )
# source: https://stackoverflow.com/a/35756195/256662
from functools import reduce
def _reduce_concat(x, sep=""):
    return reduce(lambda x, y: str(x) + sep + str(y), x)        
def paste(*lists, sep=" ", collapse=None):
    result = map(lambda x: _reduce_concat(x, sep=sep), zip(*lists))
    if collapse is not None:
        return _reduce_concat(result, sep=collapse)
    return list(result)
# this fails with recycling:
# paste("Hello", ["Ben", "Mike"]) # ['H Ben', 'e Mike'] # not what we want.
# paste(["Hello"], ["Ben", "Mike"]) # ['Hello Ben'] # not what we want.
# paste("a", ["Ben", "Mike"]) # ['a Ben'] # not what we want.
# gets all the py files from a root folder (excluding tests)
def get_all_py_files_no_tests(root):
    result = []
    # based on: https://stackoverflow.com/a/2909998/256662
    for path, subdirs, files in os.walk(root):
        for name in files:
            if name[-3:] == '.py' and ('tests' not in path):
                result.append(os.path.join(path, name))
                # print(os.path.join(path, name))
    return result
def merge_py_file_and_funcs(py_file):
    import numpy as np
    funcs = get_functions_and_methods(py_file)
    py_file_recycled = np.repeat(py_file, len(funcs))
    return paste(py_file_recycled, funcs)
def flatten(l):
    """
    # source: https://stackoverflow.com/a/952952/256662
    """
    return [item for sublist in l for item in sublist]
def get_all_fun_from_root(root):
    py_files = get_all_py_files_no_tests(root)
    all_combos = flatten([merge_py_file_and_funcs(py_file) for py_file in py_files])
    return all_combos
# source: https://stackoverflow.com/a/3136703/256662
def search_replace_in_list(words, search = "", replace = ""):
    return [w.replace(search, replace) for w in words]
# search_replace_in_list(["abs", "abbfe"], "b", "_b_")
# ['a_b_s', 'a_b__b_fe']
def given_pkg_return_funs_and_methods(pkg):
    if type(pkg) is str:
        pkg_folder = pkg
    else:
        # Source: https://stackoverflow.com/a/12154601/256662
        import os
        import inspect
        pkg_folder = os.path.dirname(inspect.getfile(pkg))
        
    all_items = get_all_fun_from_root(pkg_folder)
    cleaned_all_items = search_replace_in_list(all_items, pkg_folder)
    return cleaned_all_items
Example of usage:
import numpy
given_pkg_return_funs_and_methods(numpy)
This returns:
['/__config__.py get_info(name)',
 '/__config__.py show)',
 '/_globals.py _NoValueType.__new__(cls)',
 '/_globals.py _NoValueType.__reduce__(self)',
 '/_globals.py _NoValueType.__repr__(self)',
 '/_pytesttester.py _show_numpy_info)',
 '/_pytesttester.py PytestTester.__init__(self,module_name)',
 '/_pytesttester.py PytestTester.__call__(self,label,verbose,extra_argv,doctests,coverage,durations,tests)',
 '/conftest.py pytest_configure(config)',
 '/conftest.py pytest_addoption(parser)',
 '/conftest.py pytest_sessionstart(session)',
 '/conftest.py pytest_itemcollected(item)',
 '/conftest.py check_fpu_mode(request)',
 '/conftest.py add_np(doctest_namespace)',
 '/ctypeslib.py _num_fromflags(flaglist)',
 '/ctypeslib.py _flags_fromnum(num)',
##### Etc...
Nicer printing can be done using something like:
import some_package # or use `some_package = a\direct\path`
all_fun = given_pkg_return_funs_and_methods(some_package)
for i in all_fun:
    print(i)
This solution used the following stackoverflow references: