--- /dev/null
+#!/usr/bin/env python
+#
+# freshmeat-submit -- script transactions with the Freshmeat server
+
+# This is how we sanity-check against the XML-RPC API version.
+required_major = "1"
+required_minor = "02"
+
+import sys
+
+def error(m):
+ sys.stderr.write("freshmeat-submit: %s\n" % m)
+ sys.stderr.flush()
+ sys.exit(1)
+
+if sys.version[:6] < '2.2.0':
+ error("You must upgrade to Python 2.2.0 or better to use this code.")
+
+import xmlrpclib, netrc, email.Parser, optparse, commands
+
+class FreshmeatSessionException:
+ def __init__(self, msg):
+ self.msg = msg
+
+class FreshmeatSession:
+ "Encapsulate the state of a Freshmeat session."
+ freshmeat_xmlrpc = "http://freshmeat.net/xmlrpc/"
+
+ def __init__(self, login=None, password=None, verbose=0):
+ "Initialize an XML-RPC session to Freshmeat by logging in."
+ self.verbose = verbose
+ # If user didn't supply credentials, fetch from ~/.netrc
+ if not login:
+ try:
+ credentials = netrc.netrc()
+ except netrc.NetrcParseError, e:
+ raise FreshmeatSessionException("ill-formed .netrc: %s:%s %s" \
+ % (e.filename, e.lineno, e.msg))
+ except IOError, e:
+ raise FreshmeatSessionException(("missing .netrc file %s" % \
+ str(e).split()[-1]))
+ ret = credentials.authenticators("freshmeat")
+ if not ret:
+ raise FreshmeatSessionException("no credentials for Freshmeat")
+ login, account, password = ret
+ # Open xml-rpc connection to Freshmeat
+ self.session = xmlrpclib.Server(FreshmeatSession.freshmeat_xmlrpc,
+ verbose=verbose)
+ # Log in to Freshmeat
+ response = self.session.login( \
+ {"username":login, "password":password})
+ self.sid = response['SID']
+ self.lifetime = response['Lifetime']
+ api_version = response['API Version']
+ # Sanity-check against the version
+ (major, minor) = api_version.split(".")
+ if major != required_major or minor < required_minor:
+ FreshmeatSessionException("this version is out of date; get a replacement from Freshmeat.")
+ if self.verbose:
+ print "Session ID = %s, lifetime = %s" % (self.sid, self.lifetime)
+
+ def get_branch_list(self, name):
+ "Get the branch list for the current project."
+ if self.verbose:
+ print "About to look up project"
+ self.branch_list = self.session.fetch_branch_list({'SID':self.sid,'project_name':name})
+ if self.verbose:
+ print "Project branch list is:", self.branch_list
+
+ def publish_release(self, data):
+ "Add a new release to the current project."
+ response = self.session.publish_release(data)
+ if "OK" not in response:
+ raise FreshmeatSessionException(response)
+
+ def withdraw_release(self, release):
+ response = self.session.withdraw_release(release)
+ if "OK" not in response:
+ raise FreshmeatSessionException(response)
+
+ def logout(self):
+ "End the session."
+ return self.session.logout({"SID":self.sid})
+
+freshmeat_field_map = (
+ ("Project", "p", "project_name"),
+ ("Branch", "b", "branch_name"),
+ ("Version", "v", "version"),
+ ("Changes", "c", "changes"),
+ ("Release-Focus", "r", "release_focus"),
+ ("Hide", "x", "hide_from_frontpage"),
+ ("License", "l", "license"),
+ ("Home-Page-URL", "H", "url_homepage"),
+ ("Gzipped-Tar-URL", "G", "url_tgz"),
+ ("Bzipped-Tar-URL", "B", "url_bz2"),
+ ("Zipped-Tar-URL", "Z", "url_zip"),
+ ("Changelog-URL", "C", "url_changelog"),
+ ("RPM-URL", "R", "url_rpm"),
+ ("Debian-URL", "D", "url_deb"),
+ ("OSX-URL", "O", "url_osx"),
+ ("BSD-Port-URL", "P", "url_bsdport"),
+ ("Purchase-URL", "U", "url_purchase"),
+ ("CVS-URL", "S", "url_cvs"),
+ ("Mailing-List-URL", "L", "url_list"),
+ ("Mirror-Site-URL", "M", "url_mirror"),
+ ("Demo-URL", "E", "url_demo"),
+ )
+
+def get_rpm_field(fld, rpm):
+ cmd = "rpm --queryformat='%%{%s}' -qp %s" % (fld, rpm)
+ (status, output) = commands.getstatusoutput(cmd)
+ if status != 0:
+ raise ValueError
+ return output
+
+def crack_rpm(rpm, dict):
+ "Extract freshmeat metadata from an RPM."
+ try:
+ # Some fields can be copied literally if present.
+ if not "project_name" in dict:
+ dict["project_name"] = get_rpm_field("name", rpm)
+ if not "version" in dict:
+ dict["version"] = get_rpm_field("version", rpm)
+ # This doesn't work. The values don't map over.
+ #if not "license" in dict:
+ # dict["license"] = get_rpm_field("license", rpm)
+ if not "url_homepage" in dict:
+ dict["url_homepage"] = get_rpm_field("url", rpm)
+ if not "changes" in dict:
+ # Querying gets you the first entry, apparently
+ # blank-line-delimited.
+ changes = get_rpm_field("changelogtext", rpm)
+ # Canonicalize, stripping leading spaces.
+ changes = map(lambda x: x.strip(), changes.split('\n'))
+ # There's a 600-char-limit on Freshmeat changes fields.
+ changes = "\n".join(changes)[:599] + "\n"
+ dict["changes"] = changes
+ # RPMs have a source field; figure out which Freshmeat field it maps to
+ source = get_rpm_field("source", rpm)
+ if source.endswith(".tar.gz") or source.endswith(".tgz"):
+ if "url_tgz" not in dict:
+ dict["url_tgz"] = source
+ if source.endswith(".tar.bz2"):
+ if "url_bz2" not in dict:
+ dict["url_bz2"] = source
+ except ValueError:
+ pass
+
+class FreshmeatMetadataFactory:
+ "Factory class for producing Metadata records"
+
+ 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 freshmeat_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('-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', '--noemit', dest='noemit',
+ 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.')
+
+ def header_to_field(self, hdr):
+ lhdr = hdr.lower().replace("-", "_")
+ for (alias, shortopt, field) in freshmeat_field_map:
+ if lhdr == alias.lower().replace("-", "_"):
+ return field
+ raise FreshmeatSessionException("Illegal field name %s" % hdr)
+
+ def getMetadata(self, stream):
+ "Make a Metadata object."
+ data = {}
+ (options, args) = self.argument_parser.parse_args()
+ # First stuff from rpms if present.
+ for file in args:
+ if file.endswith(".rpm"):
+ crack_rpm(file, data)
+ # Second. stuff fropm stdin if present
+ if options.read:
+ message = self.message_parser.parse(stream)
+ for (key, value) in message.items():
+ data.update({self.header_to_field(key) : value})
+ if not 'changes' in data:
+ data['changes'] = message.get_payload()
+ # Merge in options from the command line;
+ # they override what's on stdin.
+ data['verbose'] = False
+ for (key, value) in options.__dict__.items():
+ if key != 'read' and value != None:
+ data[key] = value
+ # Release-Focus field may have to be validated later.
+ if "release_focus" in data:
+ val = data["release_focus"].lower()
+ if val in '123456789':
+ data["release_focus"] = int(val)
+ return data
+
+ def validate(self, session, verbose="1"):
+ "Validate a metadata object using the Freshmeat server"
+ # Validate Release-Focus field if present
+ focus = data.get('release_focus')
+ if focus and type(focus) != type(0):
+ if not session:
+ session = FreshmeatSession(verbose=int(verbose))
+ freshmeat_foci = session.session.fetch_available_release_foci()
+ if focus in freshmeat_foci:
+ data["release_focus"] = freshmeat_foci[focus]
+ else:
+ raise FreshmeatSessionException("focus type error: needs one of" + `freshmeat_foci.keys()`)
+ # Validate License field if present
+ license = data.get('license')
+ if license:
+ if not session:
+ session = FreshmeatSession(verbose=int(verbose))
+ freshmeat_licenses = session.session.fetch_available_licenses()
+ freshmeat_licenses = map(lambda x: x.lower(), freshmeat_licenses)
+ if license.lower() not in freshmeat_licenses:
+ raise FreshmeatSessionException("license error: needs one of " + `freshmeat_foci`)
+ # All OK
+ return session
+
+if __name__ == "__main__":
+ try:
+ session = None
+ # First, gather update data from stdin and command-line switches
+ factory = FreshmeatMetadataFactory()
+ data = factory.getMetadata(sys.stdin)
+ # Some switches shouldn't be passed to the server
+ verbose = data['verbose']; del data['verbose']
+ delete = data['delete']; del data['delete']
+ noemit = data['noemit']; del data['noemit']
+ # Validate any fields we can check with the server
+ session = factory.validate(session, verbose)
+ # Maybe we just want to sanity-check the send
+ if noemit or verbose:
+ for (field, shortopt, key) in freshmeat_field_map:
+ if key in data and key != 'changes':
+ print "%s: %s" % (field, data[key])
+ if 'changes' in data:
+ sys.stdout.write("\n" + data['changes'])
+ # Time to ship the update.
+ if not noemit:
+ # Establish session
+ if not session:
+ session = FreshmeatSession(verbose=int(verbose))
+ # OK, now actually add the release.
+ data['SID'] = session.sid
+ if delete:
+ session.withdraw_release(data)
+ else:
+ session.publish_release(data)
+ # Log out
+ session.logout()
+ except FreshmeatSessionException, e:
+ print >>sys.stderr,"freshmeat-submit:", e.msg
+ sys.exit(1)
+ except xmlrpclib.Fault, f:
+ print >>sys.stderr,"freshmeat-submit: %d %s" % (f.faultCode, f.faultString)
+ sys.exit(1)
+
+# end
+