comparison py/rlgalldb.py @ 30:e8f3b7994d88

Port to Python 3. All scripts and modules have been ported to Python 3 and appear to work. Some changes to the lighttpd configuration were needed.
author John "Elwin" Edwards
date Tue, 31 Dec 2013 13:36:19 -0500
parents 453278a81371
children 7303535b5a5d
comparison
equal deleted inserted replaced
29:23a769aa487e 30:e8f3b7994d88
1 # rlgalldb.py 1 # rlgalldb.py
2 # Module for the Roguelike Gallery, using a postgres database 2 # Module for the Roguelike Gallery, using a postgres database
3 # Requires Python 3.3
3 4
4 import os 5 import os
5 import calendar
6 import psycopg2 6 import psycopg2
7 from datetime import datetime, tzinfo, timedelta 7 from datetime import datetime
8 import pytz
8 9
9 # Configuration 10 # Configuration
10 logdir = "/var/dgl/var/games/roguelike/" 11 logdir = "/var/dgl/var/games/roguelike/"
11 webdir = "/var/www/lighttpd/scoring/" 12 webdir = "/var/www/lighttpd/scoring/"
12 ppagename = webdir + "players/{0}.html" 13 ppagename = webdir + "players/{0}.html"
47 "fate":"Fate", "rank":"Rank", "game":"Game", "class": "Class"} 48 "fate":"Fate", "rank":"Rank", "game":"Game", "class": "Class"}
48 # Queries for the games table 49 # Queries for the games table
49 offselstr = "SELECT offbytes FROM games WHERE gname = %s;" 50 offselstr = "SELECT offbytes FROM games WHERE gname = %s;"
50 newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;" 51 newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;"
51 52
52 # A representation of the UTC time zone. They say Py3k can better handle
53 # this madness.
54 class UTC(tzinfo):
55 def utcoffset(self, dt):
56 return timedelta(0)
57 def dst(self, dt):
58 return timedelta(0)
59 def tzname(self, dt):
60 return "UTC"
61 utc = UTC()
62
63 def getconn(): 53 def getconn():
64 "Returns a database connection, or None if the connection fails." 54 "Returns a database connection, or None if the connection fails."
65 try: 55 try:
66 conn = psycopg2.connect("dbname=rlg") 56 conn = psycopg2.connect("dbname=rlg")
67 except psycopg2.OperationalError: 57 except psycopg2.OperationalError:
69 return conn 59 return conn
70 60
71 def recnameToTS(filename): 61 def recnameToTS(filename):
72 pattern = "%Y-%m-%d.%H:%M:%S.ttyrec" 62 pattern = "%Y-%m-%d.%H:%M:%S.ttyrec"
73 try: 63 try:
74 dt = datetime.strptime(filename, pattern).replace(tzinfo=utc) 64 dt = datetime.strptime(filename, pattern).replace(tzinfo=pytz.utc)
75 return dt 65 return dt
76 except ValueError: 66 except ValueError:
77 return None 67 return None
78 68
79 def ttyreclink(text, name, game, gtime): 69 def ttyreclink(text, name, game, gtime):
88 78
89 def linktoArchive(entry): 79 def linktoArchive(entry):
90 "Takes an entry dict and returns a link to the ttyrec archivist." 80 "Takes an entry dict and returns a link to the ttyrec archivist."
91 lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>' 81 lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>'
92 linktext = entry["endt"].strftime("%Y/%m/%d %H:%M:%S") 82 linktext = entry["endt"].strftime("%Y/%m/%d %H:%M:%S")
93 stamp = calendar.timegm(entry["endt"].utctimetuple()) 83 stamp = int(entry["endt"].timestamp())
94 return lstr.format(entry["name"], entry["game"].uname, stamp, linktext) 84 return lstr.format(entry["name"], entry["game"].uname, stamp, linktext)
95 85
96 def maketablerow(cells, isheader=None): 86 def maketablerow(cells, isheader=None):
97 "Takes a list of strings and returns a HTML table row with each string \ 87 "Takes a list of strings and returns a HTML table row with each string \
98 in its own cell. isheader will make them header cells, obviously." 88 in its own cell. isheader will make them header cells, obviously."
154 if self.sqltypes[item] == "int": 144 if self.sqltypes[item] == "int":
155 ndict[item] = int(value) 145 ndict[item] = int(value)
156 if self.sqltypes[item] == "bool": 146 if self.sqltypes[item] == "bool":
157 ndict[item] = bool(int(value)) 147 ndict[item] = bool(int(value))
158 elif self.sqltypes[item] == "timestamptz": 148 elif self.sqltypes[item] == "timestamptz":
159 ndict[item] = datetime.fromtimestamp(int(value), utc) 149 ndict[item] = datetime.fromtimestamp(int(value), pytz.utc)
160 else: 150 else:
161 ndict[item] = value 151 ndict[item] = value
162 return ndict 152 return ndict
163 def getEntryDicts(self, entfile, entlist): 153 def getEntryDicts(self, entfile, entlist):
164 "Reads logfile entries from entfile, interprets them according to the \ 154 "Reads logfile entries from entfile, interprets them according to the \
179 # Get the previous offset 169 # Get the previous offset
180 cur.execute(offselstr, [self.uname]) 170 cur.execute(offselstr, [self.uname])
181 offset = cur.fetchone()[0] 171 offset = cur.fetchone()[0]
182 newlist = [] 172 newlist = []
183 try: 173 try:
184 scr = open(self.scores) 174 scr = open(self.scores, encoding="utf-8")
185 scr.seek(offset) 175 scr.seek(offset)
186 self.getEntryDicts(scr, newlist) 176 self.getEntryDicts(scr, newlist)
187 except IOError: 177 except IOError:
188 noffset = offset # Can't read anything, assume no new games 178 noffset = offset # Can't read anything, assume no new games
189 else: 179 else:
213 cur.execute(tquery, [nameF]) 203 cur.execute(tquery, [nameF])
214 result = cur.fetchone() 204 result = cur.fetchone()
215 if result: 205 if result:
216 prev = result[0] 206 prev = result[0]
217 else: 207 else:
218 prev = datetime.fromtimestamp(0, utc); 208 prev = datetime.fromtimestamp(0, pytz.utc);
219 ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname) 209 ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname)
220 allfilekeys = [ (recnameToTS(f), f) for f in os.listdir(ttyrecdir) ] 210 allfilekeys = [ (recnameToTS(f), f) for f in os.listdir(ttyrecdir) ]
221 vfilekeys = [ e for e in allfilekeys if e[0] > prev ] 211 vfilekeys = [ e for e in allfilekeys if e[0] > prev ]
222 vfilekeys.sort(key=lambda e: e[0]) 212 vfilekeys.sort(key=lambda e: e[0])
223 # Now determine startt and ttyrecs for each game 213 # Now determine startt and ttyrecs for each game
240 return 230 return
241 def tablerecent(self, of): 231 def tablerecent(self, of):
242 "Prints the most recent games from the logfile, NOT the database." 232 "Prints the most recent games from the logfile, NOT the database."
243 newest = [] 233 newest = []
244 try: 234 try:
245 scr = open(self.scores) 235 scr = open(self.scores, encoding="utf-8")
246 except IOError: 236 except FileNotFoundError:
247 pass 237 pass
248 else: 238 else:
239 # Text streams don't support random seeking.
249 try: 240 try:
250 scr.seek(self.lookback, 2) 241 scr.buffer.seek(self.lookback, 2)
251 except IOError: 242 except OSError:
252 scr.seek(0) # The file wasn't that long, start at the beginning 243 scr.buffer.seek(0) # The file wasn't that long, start at the beginning
253 if scr.tell() != 0: 244 if scr.buffer.tell() != 0:
254 scr.readline() # Throw away the incomplete line 245 scr.buffer.readline() # Throw away the incomplete line
255 self.getEntryDicts(scr, newest) 246 self.getEntryDicts(scr, newest)
256 newest.reverse() 247 newest.reverse()
257 scr.close() 248 scr.close()
258 of.write(secthead.format(self.name)) 249 of.write(secthead.format(self.name))
259 if not newest: 250 if not newest:
272 self.sqltypes = {"endt": "timestamptz", "score": "int", 263 self.sqltypes = {"endt": "timestamptz", "score": "int",
273 "name": "varchar(20)", "xl": "int", "fate": "text", 264 "name": "varchar(20)", "xl": "int", "fate": "text",
274 "ttyrecs": "text ARRAY", "startt": "timestamptz"} 265 "ttyrecs": "text ARRAY", "startt": "timestamptz"}
275 self.logdelim = " " 266 self.logdelim = " "
276 self.lookback = -1500 267 self.lookback = -1500
277 colspec = "(" 268 # Construct the insert query
278 valspec = "(" 269 fields = self.sqltypes.keys()
279 for i, col in enumerate(self.sqltypes.keys()): 270 colspec = ", ".join(fields)
280 colspec += col 271 valspec = ", ".join([ "%({})s".format(c) for c in fields ])
281 valspec += "%({0})s".format(col) 272 self.insertq = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname,
282 if i == len(self.sqltypes) - 1:
283 colspec += ")"
284 valspec += ")"
285 else:
286 colspec += ", "
287 valspec += ", "
288 self.insertq = "INSERT INTO {0} {1} VALUES {2};".format(self.uname,
289 colspec, valspec) 273 colspec, valspec)
290 # Class variables, used by some methods 274 # Class variables, used by some methods
291 fields = ["name", "score", "xl", "fate", "endt"] 275 fields = ["name", "score", "xl", "fate", "endt"]
292 rankfields = ["rank", "score", "name", "xl", "fate", "endt"] 276 rankfields = ["rank", "score", "name", "xl", "fate", "endt"]
293 pfields = ["score", "xl", "fate", "endt"] 277 pfields = ["score", "xl", "fate", "endt"]
378 conn.close() 362 conn.close()
379 return entries 363 return entries
380 def putIntoDB(self, dictlist, conn): 364 def putIntoDB(self, dictlist, conn):
381 "Add the entries in dictlist to the database through connection conn, \ 365 "Add the entries in dictlist to the database through connection conn, \
382 which needs the INSERT privilege." 366 which needs the INSERT privilege."
383 # FIXME this monster needs to be procedurally generated 367 fields = self.sqltypes.keys()
384 qstr = "INSERT INTO " + self.uname + " (endt, score, name, xl, fate, ttyrecs, startt) \ 368 fstr = ", ".join(fields)
385 VALUES (%(endt)s, %(score)s, %(name)s, %(xl)s, %(fate)s, %(ttyrecs)s, %(startt)s);" 369 vstr = ", ".join([ "%({})s".format(c) for c in fields ])
386 cur = conn.cursor() 370 qstr = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname, fstr, vstr);
387 cur.executemany(qstr, [ d for d in dictlist if d["game"] == self ]) 371 cur = conn.cursor()
372 cur.executemany(self.insertq, [ d for d in dictlist if d["game"] == self ])
388 conn.commit() 373 conn.commit()
389 cur.close() 374 cur.close()
390 return 375 return
391 # End RogueGame class definition 376 # End RogueGame class definition
392 377
401 "name": "varchar(20)", "xl": "int", "class": "text", "depth": "int", 386 "name": "varchar(20)", "xl": "int", "class": "text", "depth": "int",
402 "maxdepth": "int", "quest": "int", "hadquest": "bool", 387 "maxdepth": "int", "quest": "int", "hadquest": "bool",
403 "fate": "text", "ttyrecs": "text ARRAY", "startt": "timestamptz"} 388 "fate": "text", "ttyrecs": "text ARRAY", "startt": "timestamptz"}
404 self.logdelim = " " 389 self.logdelim = " "
405 self.lookback = -1800 390 self.lookback = -1800
406 colspec = "(" 391 # Construct the insert query
407 valspec = "(" 392 fields = self.sqltypes.keys()
408 for i, col in enumerate(self.sqltypes.keys()): 393 colspec = ", ".join(fields)
409 colspec += col 394 valspec = ", ".join([ "%({})s".format(c) for c in fields ])
410 valspec += "%({0})s".format(col) 395 self.insertq = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname,
411 if i == len(self.sqltypes) - 1:
412 colspec += ")"
413 valspec += ")"
414 else:
415 colspec += ", "
416 valspec += ", "
417 self.insertq = "INSERT INTO {0} {1} VALUES {2};".format(self.uname,
418 colspec, valspec) 396 colspec, valspec)
419 # Class variables 397 # Class variables
420 fields = ["name", "score", "class", "xl", "fate", "endt"] 398 fields = ["name", "score", "class", "xl", "fate", "endt"]
421 rankfields = ["rank", "score", "name", "class", "xl", "fate", "endt"] 399 rankfields = ["rank", "score", "name", "class", "xl", "fate", "endt"]
422 pfields = ["score", "class", "xl", "fate", "endt"] 400 pfields = ["score", "class", "xl", "fate", "endt"]
490 gamelist = [rogue3, rogue4, rogue5, srogue, arogue5] 468 gamelist = [rogue3, rogue4, rogue5, srogue, arogue5]
491 469
492 def playerpage(pname): 470 def playerpage(pname):
493 "Generate a player's HTML page" 471 "Generate a player's HTML page"
494 # Write the beginning of the page 472 # Write the beginning of the page
495 ppagefi = open(ppagename.format(pname), "w") 473 ppagefi = open(ppagename.format(pname), "w", encoding="utf-8")
496 ppagefi.write(phead.format(pname)) 474 ppagefi.write(phead.format(pname))
497 ppagefi.write(ptop) 475 ppagefi.write(ptop)
498 ppagefi.write(navplayer.format(pname)) 476 ppagefi.write(navplayer.format(pname))
499 ppagefi.write(pti.format("Results for " + pname)) 477 ppagefi.write(pti.format("Results for " + pname))
500 for game in gamelist: 478 for game in gamelist:
507 entries.sort(key=lambda e: e["endt"]) 485 entries.sort(key=lambda e: e["endt"])
508 printTable(entries, game.pfields, ppagefi) 486 printTable(entries, game.pfields, ppagefi)
509 scoresum = 0 487 scoresum = 0
510 for entry in entries: 488 for entry in entries:
511 scoresum += int(entry["score"]) 489 scoresum += int(entry["score"])
512 avgscr = scoresum / len(entries) 490 avgscr = scoresum // len(entries)
513 ppagefi.write("<p>Average score: {0}</p>\n".format(avgscr)) 491 ppagefi.write("<p>Average score: {0}</p>\n".format(avgscr))
514 # That should settle it. Print the end; then stop. 492 # That should settle it. Print the end; then stop.
515 ppagefi.write(pend) 493 ppagefi.write(pend)
516 ppagefi.close() 494 ppagefi.close()
517 return 495 return
518 496
519 def highpage(): 497 def highpage():
520 # open the page and print the beginning 498 # open the page and print the beginning
521 highpfi = open(hpagename, "w") 499 highpfi = open(hpagename, "w", encoding="utf-8")
522 highpfi.write(phead.format("High Scores")) 500 highpfi.write(phead.format("High Scores"))
523 highpfi.write(ptop) 501 highpfi.write(ptop)
524 highpfi.write(navscore.format("High Scores")) 502 highpfi.write(navscore.format("High Scores"))
525 highpfi.write(pti.format("List of High Scores")) 503 highpfi.write(pti.format("List of High Scores"))
526 for game in gamelist: 504 for game in gamelist: