#!/usr/bin/env python """Load respondent data from a file and analyze it. The input must be in this format (blank lines, casing, and leading/trailing whitespace are ignored): ~~~~ John Doe 10 Python 9 Django 8 Postgres 7 HTML 6 CSS 5 JavaScript 4 JQuery 3 Linux 2 Apache 1 SOAP 1 REST 1 VI3/vSphere 1 LabManager ~~~~ John Smith [jsmith] ... Missing skills default to 1. All Person objects are named using the lowercase first name; if this conflicts, specify a "short name" in brackets. Once loaded, you'll get a Python prompt with the following objects defined: ls list all applicants, sorted by (weighted) average ls() list all applicants, sorted by , which is one of "average" or "name" show detail for a particular applicant, sorted by skill level () show detail for a particular applicant, sorted by , which is one of "importance", "level", or "name" Here's an example session: $ ./crunch.py responses.txt Greetings, program! 'ls' to list, 'shortname' to drill down. >>> ls Avg Name [shortname] ================================================== 5.72 Chad Whitacre [chad] 5.65 Chad Smith [csmith] >>> chad Chad Whitacre ============= 8 Python 8 HTML 8 CSS 7 Linux 7 Django 6 JavaScript 6 JQuery 6 Apache 4 Postgres 3 REST 2 SOAP 1 VI3/vSphere 1 LabManager >>> ls('name') Avg Name [shortname] ================================================== 5.65 Chad Smith [csmith] 5.72 Chad Whitacre [chad] >>> csmith('importance') Chad Smith ========== 7 Python 7 Django 4 Postgres 8 HTML 8 CSS 6 JavaScript 6 JQuery 7 Linux 6 Apache 2 SOAP 1 REST 1 VI3/vSphere 2 LabManager >>> """ from __future__ import division import code import os import sys __author__ = "Chad Whitacre " __resume__ = "http://www.whit537.org/resume/" NEW_PERSON = '~~~~ ' SKILLS = [ (10, "Python") # the ints are weights; tweak as needed , (10, "Django") , ( 9, "Postgres") , ( 8, "HTML") , ( 8, "CSS") , ( 7, "JavaScript") , ( 7, "JQuery") , ( 6, "Linux") , ( 6, "Apache") , ( 5, "SOAP") , ( 5, "REST") , ( 4, "VI3/vSphere") , ( 4, "LabManager") ] SKILLS_SORTED = sorted(SKILLS, key=lambda s: s[1]) WEIGHT = sum(s[0] for s in SKILLS) # Object Model # ============ class Person(dict): """Model a person applying for this job. Keys are skill names. >>> jimmy = Person('Jimmy Wales', 'jimmy') >>> jimmy['python'] = 10 >>> jimmy.average """ def __init__(self, name, short): self.name = name self.short = short for foo, name in SKILLS: self[name.lower()] = 1 # skills default to 1 def __str__(self): return self._by_level() __repr__ = __str__ def __call__(self, sort='importance'): try: func = getattr(self, '_by_'+sort) except AttributeError: print "Sort key must be 'importance', 'level', or 'name'." else: print func() def _by_importance(self, display=True): return self._detail([skill[1] for skill in SKILLS]) def _by_name(self): return self._detail([skill[1] for skill in SKILLS_SORTED]) def _by_level(self): zipped = zip( [s[1] for s in sorted(self.items())] , [s[1] for s in SKILLS_SORTED] ) zipped.sort() zipped.reverse() names = [tup[1] for tup in zipped] return self._detail(names) def _detail(self, names): out = ['', self.name, '='*len(self.name)] for name in names: out.append("%2d %s" % (self[name.lower()], name)) out.append('') out = os.linesep.join(out) return out @property def average(self): """Return the weighted average of this person's skills. """ average = (sum(s[0] * self[s[1].lower()] for s in SKILLS) / WEIGHT) return "%.02f" % average class Lister: """Support using 'ls' from the console. """ def __init__(self, people): self.people = people def __repr__(self): return self(display=False) def __call__(self, sort='average', display=True): if sort == 'average': sig = dict(key=lambda p: p.average, reverse=True) elif sort == 'name': sig = dict(key=lambda p: p.name) else: return "Sort key must be either 'average' or 'name'." out = self._detail(**sig) if display: print out else: return out def _detail(self, **sig): out = ['', " Avg Name [shortname]", "="*50] for p in sorted(self.people.values(), **sig): out.append("%5s %s [%s]" % (p.average, p.name, p.short)) out.append('') return os.linesep.join(out) # Parser # ====== def parse(input): """Given a file-like object, return a dictionary of Person objects. """ person = None people = dict() for line in input: line = line.strip() if not line: continue # Parse a person line. # ==================== # Ensure that the first line names a person. if line.startswith(NEW_PERSON): name = line[len(NEW_PERSON):] if '[' in name: name, short = name.split('[') name = name.strip() short = short.rstrip(']') else: short = ' ' in name and name.split()[0] or short short = short.lower() if short in people: raise StandardError("Two people with short name '%s'." % short) people[short] = person = Person(name, short) elif person is None: raise StandardError("No person named before skills listed.") # Parse a skill line. # =================== else: try: level, skill = line.lower().split() level = int(level) except: raise StandardError("Unable to parse skill line: %s" % line) person[skill] = level return people # Main # ==== def main(): try: filename = sys.argv[1] fp = open(filename, 'r') except IndexError: raise SystemExit("Usage: %s " % sys.argv[0]) except IOError: raise SystemExit("Unable to open '%s'" % sys.argv[1]) people = parse(fp) namespace = dict(people) # copy so we don't try to list the 'ls' object! namespace['ls'] = Lister(people) banner = "Greetings, program! 'ls' to list, 'shortname' to drill down." code.interact(banner, local=namespace) if __name__ == '__main__': try: main() except Exception, exc: print >> sys.stderr, "Error:", exc.args[0] sys.exit(1)