Assorted project boilerplate.
[naja.git] / pyweek_upload.py
diff --git a/pyweek_upload.py b/pyweek_upload.py
new file mode 100644 (file)
index 0000000..f9201be
--- /dev/null
@@ -0,0 +1,223 @@
+'''
+Upload script specifically engineered for the PyWeek challenge.
+
+Handles authentication and gives upload progress feedback.
+'''
+import sys
+import os
+import httplib
+import cStringIO
+import socket
+import time
+import getopt
+
+
+class Upload(object):
+    def __init__(self, filename):
+        self.filename = filename
+
+boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
+sep_boundary = '\n--' + boundary
+end_boundary = sep_boundary + '--'
+
+
+def mimeEncode(data, sep_boundary=sep_boundary, end_boundary=end_boundary):
+    '''Take the mapping of data and construct the body of a
+    multipart/form-data message with it using the indicated boundaries.
+    '''
+    ret = cStringIO.StringIO()
+    for key, value in data.items():
+        # handle multiple entries for the same name
+        if not isinstance(value, list):
+            value = [value]
+        for value in value:
+            ret.write(sep_boundary)
+            if isinstance(value, Upload):
+                ret.write('\nContent-Disposition: form-data; name="%s"' % key)
+                filename = os.path.basename(value.filename)
+                ret.write('; filename="%s"\n\n' % filename)
+                value = open(os.path.join(value.filename), "rb").read()
+            else:
+                ret.write('\nContent-Disposition: form-data; name="%s"' % key)
+                ret.write("\n\n")
+                value = str(value)
+            ret.write(str(value))
+            if value and value[-1] == '\r':
+                ret.write('\n')  # write an extra newline
+    ret.write(end_boundary)
+    return ret.getvalue()
+
+
+class Progress(object):
+    def __init__(self, info, data):
+        self.info = info
+        self.tosend = len(data)
+        self.total = self.tosend / 1024
+        self.data = cStringIO.StringIO(data)
+        self.start = self.now = time.time()
+        self.sent = 0
+        self.num = 0
+        self.stepsize = self.total / 100 or 1
+        self.steptimes = []
+        self.display()
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        self.num += 1
+        if self.sent >= self.tosend:
+            print self.info, 'done', ' ' * (75 - len(self.info) - 6)
+            sys.stdout.flush()
+            raise StopIteration
+
+        chunk = self.data.read(1024)
+        self.sent += len(chunk)
+        #print (self.num, self.stepsize, self.total, self.sent, self.tosend)
+
+        if self.num % self.stepsize:
+            return chunk
+        self.display()
+        return chunk
+
+    def display(self):
+        # figure how long we've spent - guess how long to go
+        now = time.time()
+        steptime = now - self.now
+        self.steptimes.insert(0, steptime)
+        if len(self.steptimes) > 5:
+            self.steptimes.pop()
+        steptime = sum(self.steptimes) / len(self.steptimes)
+        self.now = now
+        eta = steptime * ((self.total - self.num) / self.stepsize)
+
+        # tell it like it is (or might be)
+        if now - self.start > 3:
+            M = eta / 60
+            H = M / 60
+            M = M % 60
+            S = eta % 60
+            if self.total:
+                s = '%s %2d%% (ETA %02d:%02d:%02d)' % (self.info,
+                    self.num * 100. / self.total, H, M, S)
+            else:
+                s = '%s 0%% (ETA %02d:%02d:%02d)' % (self.info, H, M, S)
+        elif self.total:
+            s = '%s %2d%%' % (self.info, self.num * 100. / self.total)
+        else:
+            s = '%s %d done' % (self.info, self.num)
+        sys.stdout.write(s + ' ' * (75 - len(s)) + '\r')
+        sys.stdout.flush()
+
+
+class progressHTTPConnection(httplib.HTTPConnection):
+    def progress_send(self, str):
+        """Send `str' to the server."""
+        if self.sock is None:
+            self.connect()
+
+        p = Progress('Uploading', str)
+        for chunk in p:
+            sent = 0
+            while sent != len(chunk):
+                try:
+                    sent += self.sock.send(chunk)
+                except socket.error, v:
+                    if v[0] == 32:      # Broken pipe
+                        self.close()
+                    raise
+                p.display()
+
+
+class progressHTTP(httplib.HTTP):
+    _connection_class = progressHTTPConnection
+
+    def _setup(self, conn):
+        httplib.HTTP._setup(self, conn)
+        self.progress_send = self._conn.progress_send
+
+
+def http_request(data, server, port, url):
+    h = progressHTTP(server, port)
+
+    data = mimeEncode(data)
+    h.putrequest('POST', url)
+    h.putheader('Content-type', 'multipart/form-data; boundary=%s' % boundary)
+    h.putheader('Content-length', str(len(data)))
+    h.putheader('Host', server)
+    h.endheaders()
+
+    h.progress_send(data)
+
+    errcode, errmsg, headers = h.getreply()
+
+    f = h.getfile()
+    response = f.read().strip()
+    f.close()
+
+    print '%s %s' % (errcode, errmsg)
+    if response:
+        print response
+
+
+def usage():
+    print '''This program is to be used to upload files to the PyWeek system.
+You may use it to upload screenshots or code submissions.
+
+REQUIRED ARGUMENTS:
+ -u   username
+ -p   password
+ -d   description of file
+ -c   file to upload
+ -e   entry short name
+
+OPTIONAL ARGUMENTS:
+ -s   file is a screenshot
+ -f   file is FINAL submission
+ -h   override default host name (www.pyweek.org)
+ -P   override default host port (80)
+
+In order to qualify for judging at the end of the challenge, you MUST
+upload your source and check the "Final Submission" checkbox.
+'''
+
+
+if __name__ == '__main__':
+    try:
+        optlist, args = getopt.getopt(sys.argv[1:], 'e:u:p:sfd:h:P:c:')
+    except getopt.GetoptError, message:
+        print message
+        usage()
+        sys.exit(1)
+    host = 'www.pyweek.org'
+    port = 80
+    data = dict(version=2)
+    optional = {}
+    url = None
+    for opt, arg in optlist:
+        if opt == '-u':
+            data['user'] = arg
+        elif opt == '-p':
+            data['password'] = arg
+        elif opt == '-s':
+            optional['is_screenshot'] = 'yes'
+        elif opt == '-f':
+            optional['is_final'] = 'yes'
+        elif opt == '-d':
+            data['description'] = arg
+        elif opt == '-c':
+            data['content_file'] = Upload(arg)
+        elif opt == '-e':
+            url = '/e/%s/oup/' % arg
+        elif opt == '-h':
+            host = arg
+        elif opt == '-P':
+            port = int(arg)
+
+    if len(data) < 4 or url is None:
+        print 'Required argument missing'
+        usage()
+        sys.exit(1)
+
+    data.update(optional)
+    http_request(data, host, port, url)