def parse_changelog(changes_path):
    """Parse the given changelog path and return `(content, parsed, nyr)`
    where `nyr` is the ' (not yet released)' marker and `parsed` looks like:

        [{'body': u'\n(nothing yet)\n\n',
          'verline': u'restify 1.0.1 (not yet released)',
          'version': u'1.0.1'},    # version is parsed out for top section only
         {'body': u'...',
          'verline': u'1.0.0'},
         {'body': u'...',
          'verline': u'1.0.0-rc2'},
         {'body': u'...',
          'verline': u'1.0.0-rc1'}]

    A changelog (CHANGES.md) is expected to look like this:

        # $project Changelog

        ## $next_version (not yet released)

        ...

        ## $version1

        ...

        ## $version2

        ... and so on

    The version lines are enforced as follows:

    - The top entry should have a " (not yet released)" suffix. "Should"
      because recovery from half-cutarelease failures is supported.
    - A version string must be extractable from there, but it tries to
      be loose (though strict "X.Y.Z" versioning is preferred). Allowed

            ## 1.0.0
            ## my project 1.0.1
            ## foo 1.2.3-rc2

      Basically, (a) the " (not yet released)" is stripped, (b) the
      last token is the version, and (c) that version must start with
      a digit (sanity check).
    """
    if not exists(changes_path):
        raise Error("changelog file '%s' not found" % changes_path)
    content = codecs.open(changes_path, 'r', 'utf-8').read()

    parser = re.compile(
        r'^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)',
        re.M | re.S)
    sections = parser.findall(content)

    # Sanity checks on changelog format.
    if not sections:
        template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n"
        raise Error("changelog '%s' must have at least one section, "
            "suggestion:\n\n%s" % (changes_path, _indent(template)))
    first_section_verline = sections[0][0]
    nyr = ' (not yet released)'
    #if not first_section_verline.endswith(nyr):
    #    eg = "## %s%s" % (first_section_verline, nyr)
    #    raise Error("changelog '%s' top section must end with %r, "
    #        "naive e.g.: '%s'" % (changes_path, nyr, eg))

    items = []
    for i, section in enumerate(sections):
        item = {
            "verline": section[0],
            "body": section[1]
        }
        if i == 0:
            # We only bother to pull out 'version' for the top section.
            verline = section[0]
            if verline.endswith(nyr):
                verline = verline[0:-len(nyr)]
            version = verline.split()[-1]
            try:
                int(version[0])
            except __HOLE__:
                msg = ''
                if version.endswith(')'):
                    msg = " (cutarelease is picky about the trailing %r " \
                        "on the top version line. Perhaps you misspelled " \
                        "that?)" % nyr
                raise Error("changelog '%s' top section version '%s' is "
                    "invalid: first char isn't a number%s"
                    % (changes_path, version, msg))
            item["version"] = version
        items.append(item)

    return content, items, nyr

## {{{ http://code.activestate.com/recipes/577058/ (r2)
dataset/ETHPy150Open an0/Letterpress/code/markdown2/tools/cutarelease.py/parse_changelog