⌈⌋ ⎇ branch:  freshcode


Artifact [e1b3e67afa]

Artifact e1b3e67afab53188e1c690ad2de08944a69fbba8:

  • Executable file doc/fc-submit — part of check-in [3ffaf89706] at 2014-07-30 14:25:15 on branch trunk — Patched fc-submit for Python3 and using `requests` instead of `urllib2`; which is kind of necessary due to required SNI for SSL/TLS, usage of verify=cacert.pem should be possibly patched in as well (user: mario size: 16725)

#!/usr/bin/env python3
"""
freshcode-submit -- script transactions with the Freshcode.club server
"""

import sys, re, requests, json, netrc, email.parser, optparse

version = "3.0"

class Update:
    "Encapsulate dictionaries describing a project metadata update."
    def __init__(self):
        self.name = None
        self.per_project = {}
        self.urlassoc = []
        self.per_release = {}
    def __repr__(self):
        return "Update(" + repr(self.__dict__) + ")"

# The Freecode API implementation is sensitive to the kind of HTTP request
# method you use.  The general rule is:
#
# Reading records:                GET
# Adding new records:             POST
# Updating existing records:      PUT
# Deleting existing records:      DELETE
#
# From http://help.freecode.com/faqs/api-7/data-api-intro:
# 200 OK - Request was successful, the requested content is included
# 201 Created - Creation of a new resource was successful
# 401 Unauthorized - You need to provide an authentication code with this
#     request
# 403 Forbidden - You don't have permission to access this URI
# 404 Not Found - The requested resource was not found
# 409 Conflict - The validation of your submitted data failed, please check
#     the response body for error pointers
# 500 Server Error - The request hit a problem and was aborted, please report
#     as a bug if it persists
# 503 Service Unavailable - You hit your API credit limit

def RequestWithMethod(method, url, **kwargs):
    """requests.request is really a drop-in replacement here; with TLS-SNI support"""
    # Here verify="cacert.pem" would better solve the certificate issue
    return requests.request(method, url, verify=False, **kwargs)

class FreecodeSessionException(Exception):
    "Carry exception state when a session blows up."
    def __init__(self, msg):
        Exception.__init__(self)
        self.msg = msg

class FreecodeSession:
    "Encapsulate the state of a Freecode API session."
    server = "https://%s.freshcode.club/" % "api"

    def __init__(self, auth=None, verbose=0, emit_enable=True):
        "Initialize Freecode session credentials."
        self.auth = auth
        self.verbose = verbose
        self.emit_enable = emit_enable
        self.project = None
        self.permalink = None
        self.id = None
        self.project_data = None
        # If user didn't supply credentials, fetch from ~/.netrc
        if not self.auth:
            try:
                credentials = netrc.netrc()
            except netrc.NetrcParseError as e:
                raise FreecodeSessionException("ill-formed .netrc: %s:%s %s" \
                                               % (e.filename, e.lineno, e.msg))
            except IOError as e:
                raise FreecodeSessionException(("missing .netrc file %s" % \
                                                 str(e).split()[-1]))
            ret = credentials.authenticators("freshcode")
            if not ret:
                raise FreecodeSessionException("no credentials for Freshcode")
            _login, self.auth, _password = ret

    def on_project(self, name):
        "Select project by Freecode shortname."
        if self.verbose:
            print(("Selecting project: %s" % name))
        self.project = name
        pquery = FreecodeSession.server + "projects/%s.json?auth_code=%s" \
                % (self.project, self.auth)
        handle = RequestWithMethod("GET", url=pquery)
        content = json.loads(handle.text)
        self.project_data = content['project']
        #if self.verbose:
        #    print "Project data: %s" % self.project_data
        self.permalink = self.project_data['permalink']
        self.id = self.project_data['id']

    def edit_request(self, url, method="GET", request=None, force=False):
        "Wrap a JSON object with the auth code and ship it as a request"
        if request is None:
            request = {}
        url = FreecodeSession.server + url
        data = {"auth_code" : self.auth}
        data.update(request)
        data = json.dumps(data)
        headers = {"Content-Type" : "application/json"}
        if self.verbose:
            print(("Request URL:", method, url))
        #if self.verbose:
        #    print "Request headers:", headers
        if self.verbose:
            print(("Request data:", data))
        if self.emit_enable or force:
            req = RequestWithMethod(method=method,
                                    url=url,
                                    data=data,
                                    headers=headers)
            if self.verbose:
                print(req.status_code, req.url, req.headers)
            content = req.text
            if self.verbose:
                print(("Response:", content))
            return content

    def publish_release(self, data):
        "Add a new release to the current project."
        if self.verbose:
            print(("Publishing %s release: %s" % (self.project, repr(data))))
        self.edit_request("projects/" + self.permalink + "/releases.json",
                          "POST",
                          {"release": data})

    def withdraw_release(self, dversion):
        "Withdraw a specified release from the current project."
        if self.verbose:
            print("Withdrawing %s release: %s" % (self.project, dversion))
        releases = self.edit_request("projects/%s/releases/pending.json" \
                                     % self.permalink, force=True)
        releases = json.loads(releases)
        for release in releases:
            properties = release["release"]
            if properties.get("version") == dversion:
                vid = properties["id"]
                break
        else:
            raise FreecodeSessionException("couldn't find release %s"%dversion)
        deletelink = "projects/%s/releases/%s.json" % (self.permalink, vid)
        self.edit_request(deletelink, "DELETE", {})

    def update_core(self, coredata):
        "Update the core data for a project."
        if self.verbose:
            print("Core data update for %s is: %s" % (self.project, coredata))
        self.edit_request("projects/" + self.permalink + ".json",
                          "PUT",
                          {"project": coredata})

    def update_urls(self, urlassoc):
        "Update URL list for a project."
        if self.verbose:
            print("URL list update for %s is: %s" % (self.project, urlassoc))
        # First, get permalinks for all existing URLs
        uquery = FreecodeSession.server + "projects/%s/urls.json?auth_code=%s" \
                % (self.permalink, self.auth)
        handle = RequestWithMethod("GET", url=uquery)
        content = json.loads(handle.text)
        permadict = content['urls']
        # Just send the new dict over
        self.edit_request("projects/%s/urls.json" % (self.permalink),
                          "PUSH",
                          {"urls" : dict(urlassoc)})

class FreecodeMetadataFactory:
    "Factory class for producing Freecode records in JSON."
    freecode_field_map = (
        ("Project",          "P", "name"),                   # Project
        ("Summary",          "S", "oneliner"),               # Project
        ("Description",      "D", "description"),            # Project
        ("License-List",     "L", "license_list"),           # Project
        ("Project-Tag-List", "T", "project_tag_list"),       # Project
        ("Version",          "v", "version"),                # Release
        ("Changes",          "c", "changelog"),              # Release
        ("Hide",             "x", "hidden_from_frontpage"),  # Release
        ("Release-Tag-List", "t", "tag_list"),               # Release
        )
    # Which attributes have project scope, all others have release scupe
    projectwide = ('name',
                   'description',
                   'oneliner',
                   'license_list',
                   'project_tag_list')

    def __init__(self):
        self.message_parser = email.parser.Parser()
        self.argument_parser = optparse.OptionParser( \
            usage="usage: %prog [options]")
        for (msg_field, shortopt, rpc_field) in FreecodeMetadataFactory.freecode_field_map:
            self.argument_parser.add_option("-" + shortopt,
                                            "--" + msg_field.lower(),
                                            dest=rpc_field,
                                            help="Set the %s field"%msg_field)
        self.argument_parser.add_option('-q', '--query', dest='query',
                          help='Query metadata for PROJECT',metavar="PROJECT")
        self.argument_parser.add_option('-d', '--delete', dest='delete',
                          default=False, action='store_true',
                          help='Suppress reading fields from stdin.')
        self.argument_parser.add_option('-n', '--no-stdin', dest='read',
                          default=True, action='store_false',
                          help='Suppress reading fields from stdin.')
        self.argument_parser.add_option('-N', '--dryrun', dest='dryrun',
                          default=False, action='store_true',
                          help='Suppress reading fields from stdin.')
        self.argument_parser.add_option('-V', '--verbose', dest='verbose',
                          default=False, action='store_true',
                          help='Enable verbose debugging.')
        self.argument_parser.add_option('-?', '--showversion', dest='showversion',
                          default=False, action='store_true',
                          help='Show version and quit.')
    @staticmethod
    def header_to_field(hdr):
        "Map a header name from the job card format to a field."
        lhdr = hdr.lower().replace("-", "_")
        for (alias, _shortopt, field) in FreecodeMetadataFactory.freecode_field_map:
            if lhdr == alias.lower().replace("-", "_").replace("/", "_"):
                return field
        raise FreecodeSessionException("Illegal field name %s" % hdr)

    def getMetadata(self, stream):
        "Return an Update object describing project and release attributes."
        data = {}
        urls = {}
        (options, _args) = self.argument_parser.parse_args()
        # Stuff from stdin if present
        prior_version = data.get("version")
        if not (options.query or options.showversion) and options.read:
            message = self.message_parser.parse(stream)
            for (key, value) in list(message.items()):
                value = re.sub("\n +", " ", value).strip()
                if key.endswith("-URL"):
                    key = key.replace("-", " ")
                    urls.update({key[:-4] : value})
                else:
                    if key.endswith("List"):
                        value = [x.strip() for x in value.split()]
                    data.update({FreecodeMetadataFactory.header_to_field(key) : value})
            if not 'changelog' in data:
                payload = message.get_payload().strip()
                if payload:
                    data['changelog'] = payload + "\n"
            if prior_version and data.get("version") != prior_version:
                raise FreecodeSessionException("Version conflict on stdin.")
        # Merge in options from the command line;
        # they override what's on stdin.
        controls = ('query', 'delete', 'read', 'dryrun', 'verbose', 'showversion')
        prior_version = data.get("version")
        for (key, value) in list(options.__dict__.items()):
            if key not in controls and value != None:
                data[key] = value
                del options.__dict__[key]
        if prior_version and data.get("version") != prior_version and not options.delete:
            raise FreecodeSessionException("Version conflict in options.")
        # Hidden flag special handling
        if "hidden_from_frontpage" in data:
            data["hidden_from_frontpage"] = data["hidden_from_frontpage"] in ("Y", "y")
        # Now merge in the URLs, doing symbol substitution
        urllist = []
        for (label, furl) in list(urls.items()):
            for (k, v) in list(data.items()):
                if type(v) == type(""):
                    furl = furl.replace('${' + k + '}', v)
            urllist.append((label, furl))
        # Sort out what things go where
        update = Update()
        if options.showversion:
            pass
        elif options.query:
            update.name = options.query
        else:
            update.name = data.pop('name')
            update.urlassoc = urllist
            for (k, v) in list(data.items()):
                if k in FreecodeMetadataFactory.projectwide:
                    # Hack to get around a namespace collision
                    if k == "project_release_tag":
                        k = "release_tag"
                    update.per_project[k] = v
                else:
                    update.per_release[k] = v
        # Return this
        return (options, update)

if __name__ == "__main__":
    try:
        # First, gather update data from stdin and command-line switches
        factory = FreecodeMetadataFactory()
        (options, update) = factory.getMetadata(sys.stdin)
        # Some switches shouldn't be passed to the server
        query = 'query' in options.__dict__ and options.query
        verbose = 'verbose' in options.__dict__ and options.verbose
        delete  = 'delete' in options.__dict__ and options.delete
        dryrun  = 'dryrun' in options.__dict__ and options.dryrun
        showversion  = 'showversion' in options.__dict__ and options.showversion
        if showversion:
            print("freshcode-submit", version)
            raise SystemExit(0)
        # Time to ship the update.
        # Establish session
        session = FreecodeSession(verbose=int(verbose), emit_enable=not dryrun)
        try:
            session.on_project(update.name)
        except ValueError as e:
            print(e)
            print("freshcode-submit: looks like a server-side problem at freshcode.club; bailing out.", file=sys.stderr)
            raise SystemExit(1)
        if options.query:
            print("Project: %s" % session.project_data["name"])
            print("Summary: %s" % session.project_data["oneliner"])
            print("Description: %s" % session.project_data["description"].replace("\n", "\n    ").rstrip())
            print("License-List: %s" % ",".join(session.project_data["license_list"]))
            print("Project-Tag-List: %s" % ",".join(session.project_data["tag_list"]))
            for assoc in session.project_data['approved_urls']:
                #print "Querying", assoc["redirector"]
                #req = RequestWithMethod(method="HEAD",
                #                        url=assoc["redirector"],
                #                        data={},
                #                        headers={})
                #handle = urllib2.urlopen(req)
                #print "==="
                #print handle.info()
                #print "==="
                print("%s-URL: %s" % (assoc["label"].replace(" ","-"), assoc["redirector"]))
            if 'recent_releases' in session.project_data and session.project_data['recent_releases']:
                most_recent = session.project_data['recent_releases'][0]
                print("Version: %s" % most_recent['version'])
                print("Tag-List: %s" % ",".join(most_recent['tag_list']))
                if most_recent.get('hidden_from_frontpage'):
                    print("Hide: Y")
                else:
                    print("Hide: N")
                print("")
                print(most_recent["changelog"])
        else:
            # OK, now actually add or delete the release.
            if update.per_project:
                session.update_core(update.per_project)
            if update.urlassoc:
                session.update_urls(update.urlassoc)
            if delete:
                session.withdraw_release(update.per_release['version'])
            elif update.per_release and list(update.per_release.keys())!=["version"]:
                session.publish_release(update.per_release)
    except FreecodeSessionException as e:
        print("freshcode-submit:", e.msg, file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.HTTPError as f:
        print("freshcode-submit: HTTPError %s" %  (f.code), file=sys.stderr)
        print(f.read(), file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.RequestException as f:
        print("freshcode-submit: URLError %s" %  (f.reason,), file=sys.stderr)
        sys.exit(1)

# end