import re


def eq_portions(actual: str, expected: str):
    """
    Compare whether actual matches portions of expected. The portions to ignore are of two types:
    - ***: ignore anything in between the left and right portions, including empty
    - +++: ignore anything in between left and right, but non-empty
    :param actual: string to test
    :param expected: expected string, containing at least one of the two patterns
    :return: a list of the portions ignored; if empty, it means there is no match.

    >>> eq_portions('', '+++aaaaaa***ccccc+++eeeeeee+++')
    ()
    >>> eq_portions('_1__aaaaaa__2__ccccc_3__eeeeeee_4_', '+++aaaaaa***ccccc+++eeeeeee+++')
    ('_1__', '__2__', '_3__', '_4_')
    >>> eq_portions('_1__aaaaaaccccc_3__eeeeeee_4_', '+++aaaaaa***ccccc+++eeeeeee+++')
    ('_1__', '', '_3__', '_4_')
    >>> eq_portions('_1__aaaaaaccccc_3__eeeeeee', '+++aaaaaa***ccccc+++eeeeeee+++')
    ()
    >>> eq_portions('aaaaaaccccc_3__eeeeeee', '+++aaaaaa***ccccc+++eeeeeee')
    ()
    >>> eq_portions('aaaaaa_1__ccccc__2_eeeeeee', '***aaaaaa***ccccc+++eeeeeee***')
    ('', '_1__', '__2_', '')
    >>> eq_portions('aaaaaa___ccccc___eeeeeee', '***aaaaaa')
    ()
    >>> eq_portions('aaaaaa___ccccc___eeeeeee', 'aaaaaa')
    Traceback (most recent call last):
      ...
    ValueError: The 'expected' argument must contain at least one *** OR +++
    """
    re_expect = re.escape(expected)
    ANYTHING = re.escape('\\*' * 3)
    SOMETHING = re.escape('\\+' * 3)
    if not re.search(ANYTHING, re_expect) and not re.search(SOMETHING, re_expect):
        raise ValueError("The 'expected' argument must contain at least one *** OR +++")

    re_expect = re.sub(SOMETHING, '(.+)', re_expect)
    re_expect = re.sub(ANYTHING, '(.*)', re_expect)
    matches = re.fullmatch(re_expect, actual)
    if not matches:
        return ()

    return matches.groups()