# Path: pytube/exceptions.py
# class ExtractError(PytubeError):
#     """Data extraction based exception."""
# 
# class RegexMatchError(ExtractError):
#     """Regex pattern did not return any matches."""
# 
#     def __init__(self, caller: str, pattern: Union[str, Pattern]):
#         """
#         :param str caller:
#             Calling function
#         :param str pattern:
#             Pattern that failed to match
#         """
#         super().__init__(f"{caller}: could not find match for {pattern}")
#         self.caller = caller
#         self.pattern = pattern
# 
# Path: pytube/helpers.py
# def cache(func: Callable[..., GenericType]) -> GenericType:
#     """ mypy compatible annotation wrapper for lru_cache"""
#     return functools.lru_cache()(func)  # type: ignore
# 
# def regex_search(pattern: str, string: str, group: int) -> str:
#     """Shortcut method to search a string for a given pattern.
# 
#     :param str pattern:
#         A regular expression pattern.
#     :param str string:
#         A target string to search.
#     :param int group:
#         Index of group to return.
#     :rtype:
#         str or tuple
#     :returns:
#         Substring pattern matches.
#     """
#     regex = re.compile(pattern)
#     results = regex.search(string)
#     if not results:
#         raise RegexMatchError(caller="regex_search", pattern=pattern)
# 
#     logger.debug("matched regex search: %s", pattern)
# 
#     return results.group(group)
# 
# Path: pytube/parser.py
# def find_object_from_startpoint(html, start_point):
#     """Parses input html to find the end of a JavaScript object.
# 
#     :param str html:
#         HTML to be parsed for an object.
#     :param int start_point:
#         Index of where the object starts.
#     :rtype dict:
#     :returns:
#         A dict created from parsing the object.
#     """
#     html = html[start_point:]
#     if html[0] not in ['{','[']:
#         raise HTMLParseError(f'Invalid start point. Start of HTML:\n{html[:20]}')
# 
#     # First letter MUST be a open brace, so we put that in the stack,
#     # and skip the first character.
#     stack = [html[0]]
#     i = 1
# 
#     context_closers = {
#         '{': '}',
#         '[': ']',
#         '"': '"'
#     }
# 
#     while i < len(html):
#         if len(stack) == 0:
#             break
#         curr_char = html[i]
#         curr_context = stack[-1]
# 
#         # If we've reached a context closer, we can remove an element off the stack
#         if curr_char == context_closers[curr_context]:
#             stack.pop()
#             i += 1
#             continue
# 
#         # Strings require special context handling because they can contain
#         #  context openers *and* closers
#         if curr_context == '"':
#             # If there's a backslash in a string, we skip a character
#             if curr_char == '\\':
#                 i += 2
#                 continue
#         else:
#             # Non-string contexts are when we need to look for context openers.
#             if curr_char in context_closers.keys():
#                 stack.append(curr_char)
# 
#         i += 1
# 
#     full_obj = html[:i]
#     return full_obj  # noqa: R504
# 
# def throttling_array_split(js_array):
#     """Parses the throttling array into a python list of strings.
# 
#     Expects input to begin with `[` and close with `]`.
# 
#     :param str js_array:
#         The javascript array, as a string.
#     :rtype: list:
#     :returns:
#         A list of strings representing splits on `,` in the throttling array.
#     """
#     results = []
#     curr_substring = js_array[1:]
# 
#     comma_regex = re.compile(r",")
#     func_regex = re.compile(r"function\([^)]*\)")
# 
#     while len(curr_substring) > 0:
#         if curr_substring.startswith('function'):
#             # Handle functions separately. These can contain commas
#             match = func_regex.search(curr_substring)
#             match_start, match_end = match.span()
# 
#             function_text = find_object_from_startpoint(curr_substring, match.span()[1])
#             full_function_def = curr_substring[:match_end + len(function_text)]
#             results.append(full_function_def)
#             curr_substring = curr_substring[len(full_function_def) + 1:]
#         else:
#             match = comma_regex.search(curr_substring)
# 
#             # Try-catch to capture end of array
#             try:
#                 match_start, match_end = match.span()
#             except AttributeError:
#                 match_start = len(curr_substring) - 1
#                 match_end = match_start + 1
# 
#             curr_el = curr_substring[:match_start]
#             results.append(curr_el)
#             curr_substring = curr_substring[match_end:]
# 
#     return results


import logging
import re
from itertools import chain
from typing import Any, Callable, Dict, List, Optional, Tuple
from pytube.exceptions import ExtractError, RegexMatchError
from pytube.helpers import cache, regex_search
from pytube.parser import find_object_from_startpoint, throttling_array_split
            return function_match.group(1)

    raise RegexMatchError(
        caller="get_initial_function_name", pattern="multiple"
    )


def get_transform_plan(js: str) -> List[str]:
    """Extract the "transform plan".

    The "transform plan" is the functions that the ciphered signature is
    cycled through to obtain the actual signature.

    :param str js:
        The contents of the base.js asset file.

    **Example**:

    ['DE.AJ(a,15)',
    'DE.VR(a,3)',
    'DE.AJ(a,51)',
    'DE.VR(a,3)',
    'DE.kT(a,51)',
    'DE.kT(a,8)',
    'DE.VR(a,3)',
    'DE.kT(a,21)']
    """
    name = re.escape(get_initial_function_name(js))
    pattern = r"%s=function\(\w\){[a-z=\.\(\"\)]*;(.*);(?:.+)}" % name
    logger.debug("getting transform plan")

# Path: pytube/exceptions.py
# class ExtractError(PytubeError):
#     """Data extraction based exception."""
# 
# class RegexMatchError(ExtractError):
#     """Regex pattern did not return any matches."""
# 
#     def __init__(self, caller: str, pattern: Union[str, Pattern]):
#         """
#         :param str caller:
#             Calling function
#         :param str pattern:
#             Pattern that failed to match
#         """
#         super().__init__(f"{caller}: could not find match for {pattern}")
#         self.caller = caller
#         self.pattern = pattern
# 
# Path: pytube/helpers.py
# def cache(func: Callable[..., GenericType]) -> GenericType:
#     """ mypy compatible annotation wrapper for lru_cache"""
#     return functools.lru_cache()(func)  # type: ignore
# 
# def regex_search(pattern: str, string: str, group: int) -> str:
#     """Shortcut method to search a string for a given pattern.
# 
#     :param str pattern:
#         A regular expression pattern.
#     :param str string:
#         A target string to search.
#     :param int group:
#         Index of group to return.
#     :rtype:
#         str or tuple
#     :returns:
#         Substring pattern matches.
#     """
#     regex = re.compile(pattern)
#     results = regex.search(string)
#     if not results:
#         raise RegexMatchError(caller="regex_search", pattern=pattern)
# 
#     logger.debug("matched regex search: %s", pattern)
# 
#     return results.group(group)
# 
# Path: pytube/parser.py
# def find_object_from_startpoint(html, start_point):
#     """Parses input html to find the end of a JavaScript object.
# 
#     :param str html:
#         HTML to be parsed for an object.
#     :param int start_point:
#         Index of where the object starts.
#     :rtype dict:
#     :returns:
#         A dict created from parsing the object.
#     """
#     html = html[start_point:]
#     if html[0] not in ['{','[']:
#         raise HTMLParseError(f'Invalid start point. Start of HTML:\n{html[:20]}')
# 
#     # First letter MUST be a open brace, so we put that in the stack,
#     # and skip the first character.
#     stack = [html[0]]
#     i = 1
# 
#     context_closers = {
#         '{': '}',
#         '[': ']',
#         '"': '"'
#     }
# 
#     while i < len(html):
#         if len(stack) == 0:
#             break
#         curr_char = html[i]
#         curr_context = stack[-1]
# 
#         # If we've reached a context closer, we can remove an element off the stack
#         if curr_char == context_closers[curr_context]:
#             stack.pop()
#             i += 1
#             continue
# 
#         # Strings require special context handling because they can contain
#         #  context openers *and* closers
#         if curr_context == '"':
#             # If there's a backslash in a string, we skip a character
#             if curr_char == '\\':
#                 i += 2
#                 continue
#         else:
#             # Non-string contexts are when we need to look for context openers.
#             if curr_char in context_closers.keys():
#                 stack.append(curr_char)
# 
#         i += 1
# 
#     full_obj = html[:i]
#     return full_obj  # noqa: R504
# 
# def throttling_array_split(js_array):
#     """Parses the throttling array into a python list of strings.
# 
#     Expects input to begin with `[` and close with `]`.
# 
#     :param str js_array:
#         The javascript array, as a string.
#     :rtype: list:
#     :returns:
#         A list of strings representing splits on `,` in the throttling array.
#     """
#     results = []
#     curr_substring = js_array[1:]
# 
#     comma_regex = re.compile(r",")
#     func_regex = re.compile(r"function\([^)]*\)")
# 
#     while len(curr_substring) > 0:
#         if curr_substring.startswith('function'):
#             # Handle functions separately. These can contain commas
#             match = func_regex.search(curr_substring)
#             match_start, match_end = match.span()
# 
#             function_text = find_object_from_startpoint(curr_substring, match.span()[1])
#             full_function_def = curr_substring[:match_end + len(function_text)]
#             results.append(full_function_def)
#             curr_substring = curr_substring[len(full_function_def) + 1:]
#         else:
#             match = comma_regex.search(curr_substring)
# 
#             # Try-catch to capture end of array
#             try:
#                 match_start, match_end = match.span()
#             except AttributeError:
#                 match_start = len(curr_substring) - 1
#                 match_end = match_start + 1
# 
#             curr_el = curr_substring[:match_start]
#             results.append(curr_el)
#             curr_substring = curr_substring[match_end:]
# 
#     return results


# Path: pytube/cipher.py
import logging
import re
from itertools import chain
from typing import Any, Callable, Dict, List, Optional, Tuple
from pytube.exceptions import ExtractError, RegexMatchError
from pytube.helpers import cache, regex_search
from pytube.parser import find_object_from_startpoint, throttling_array_split

            return function_match.group(1)

    raise RegexMatchError(
        caller="get_initial_function_name", pattern="multiple"
    )


def get_transform_plan(js: str) -> List[str]:
    """Extract the "transform plan".

    The "transform plan" is the functions that the ciphered signature is
    cycled through to obtain the actual signature.

    :param str js:
        The contents of the base.js asset file.

    **Example**:

    ['DE.AJ(a,15)',
    'DE.VR(a,3)',
    'DE.AJ(a,51)',
    'DE.VR(a,3)',
    'DE.kT(a,51)',
    'DE.kT(a,8)',
    'DE.VR(a,3)',
    'DE.kT(a,21)']
    """
    name = re.escape(get_initial_function_name(js))
    pattern = r"%s=function\(\w\){[a-z=\.\(\"\)]*;(.*);(?:.+)}" % name
    logger.debug("getting transform plan")

    return regex_search(pattern, js, group=1).split(";")