def getUrl(url): try: data = GlobalHelper.encodeAES('{"get": "peers"}') request = \ urllib.request.Request(url + "/smsgateway/api/requestrouting") request.add_header("Content-Type", "application/json;charset=utf-8") f = urllib.request.urlopen(request, data, timeout=5) if f.getcode() != 200: smsgwglobals.wislogger.debug( "Get peers NOTOK," + " Error Code: ", f.getcode()) return smsgwglobals.wislogger.debug("Get peers OK") rawbody = f.read().decode('utf-8') plaintext = GlobalHelper.decodeAES(rawbody) routelist = json.loads(plaintext) smsgwglobals.wislogger.debug(routelist) wisglobals.rdb.merge_routing(routelist) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("Get peers NOTOK") except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug( "HELPER: requestrouting socket connection timeout")
def getUrl(url): try: data = GlobalHelper.encodeAES('{"get": "peers"}') request = \ urllib.request.Request(url + "/smsgateway/api/requestrouting") request.add_header("Content-Type", "application/json;charset=utf-8") f = urllib.request.urlopen(request, data, timeout=5) if f.getcode() != 200: smsgwglobals.wislogger.debug("Get peers NOTOK," + " Error Code: ", f.getcode()) return smsgwglobals.wislogger.debug("Get peers OK") rawbody = f.read().decode('utf-8') plaintext = GlobalHelper.decodeAES(rawbody) routelist = json.loads(plaintext) smsgwglobals.wislogger.debug(routelist) wisglobals.rdb.merge_routing(routelist) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("Get peers NOTOK") except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("HELPER: requestrouting socket connection timeout")
def get_sms_stats(self): respdata = '{ "processed_sms": "N/A", "unprocessed_sms": "N/A" }' try: if wisglobals.sslenabled is not None and 'true' in wisglobals.sslenabled.lower(): request = urllib.request.Request('https://' + wisglobals.wisipaddress + ':' + wisglobals.wisport + "/api/get_sms_stats") else: request = urllib.request.Request('http://' + wisglobals.wisipaddress + ':' + wisglobals.wisport + "/api/get_sms_stats") request.add_header("Content-Type", "application/json;charset=utf-8") data = GlobalHelper.encodeAES('{"get": "sms"}') f = urllib.request.urlopen(request, data, timeout=30) resp = f.read().decode('utf-8') respdata = GlobalHelper.decodeAES(resp) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: get_sms_stats connect error") except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: get_sms_stats socket connection timeout") finally: return respdata
def received_message(self, msg): plaintext = GlobalHelper.decodeAES(str(msg)) smsgwglobals.pidlogger.debug(pidglobals.pidid + ": " + "Message received: " + str(plaintext)) data = json.loads(plaintext) if data['action'] == "sendsms": tosend = Modem.sendsms(data) plaintext = json.dumps(tosend) smsgwglobals.pidlogger.debug(pidglobals.pidid + ": " + "Message delivery status: " + str(plaintext)) message = GlobalHelper.encodeAES(plaintext) # reply sms-status to PIS self.send(message) # Exit PID on ERROR on sending sms if "ERROR" in tosend['status']: closingreason = "Modem ERROR while sending SMS!" pidglobals.closingcode = 4000 self.close(code=4000, reason=closingreason) # calculate difference time to last primary PIS check diff = datetime.now() - self.lastprimcheck # only if 5 mins are passed (= 300 sec) if diff.seconds > 300: if self.check_primpid() == "reconnect": # close Websocket to reconnect! # fixes #25 wait a bit to let pis fetch the smsstatus first time.sleep(1) closingreason = "Primary PID is back! Reinit now!" pidglobals.closingcode = 4001 self.close(code=4001, reason=closingreason) if data['action'] == "register": if data['status'] == "registered": # Start Heartbeat to connected PID hb = Heartbeat(data['modemlist'], self) hb.daemon = True hb.start() if data['action'] == "heartbeat": # connection to PIS is OK but # Response from WIS is NOT OK if data['status'] != 200: # close Connection to PIS and retry initialisation self.close()
def restartmodem(self, **params): """ json before encoding { "modemid":"00436762222222", """ # for receiving restartmodem from WIS cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) # smsgwglobals.pislogger.debug("/sendsms: rawbody: " + str(rawbody)) plaintext = GlobalHelper.decodeAES(rawbody) try: data = json.loads(plaintext) # adding action switch for message to PID data['action'] = "restartmodem" smsgwglobals.pislogger.debug("/restartmodem: dictionary: " + str(data)) except Exception as e: smsgwglobals.pislogger.warning("/restartmodem: Invalid data received! " + str(e)) cherrypy.response.status = 400 # Bad Request return try: address = PID.getclientaddress(data['modemid']) if address: # sending SMS to Pid PID.sendtopid(address, data) else: smsgwglobals.pislogger.warning("/restartmodem: No PID for " + "modem " + data['modemid'] + " found!") # If no modem endpoint to send - set own status code cherrypy.response.status = 404 return except Exception as e: smsgwglobals.pislogger.debug("/restartmodem: Internal Server " "Error! " + str(e)) cherrypy.response.status = 500 # Internal Server Error return
def received_message(self, msg): # smsgwglobals.pislogger.debug("/ws: " + str(self.peer_address) + # " - Got message: '" + str(msg) + "'") try: plaintext = GlobalHelper.decodeAES(str(msg)) # smsgwglobals.pislogger.debug("/ws: plaintext: " + plaintext) data = json.loads(plaintext) smsgwglobals.pislogger.debug("/ws: message-dictionary: " + str(data)) # check the protocol version of pid and pis if transmitted if ('pidprotocol' in data) and (data['pidprotocol'] != pisglobals.pisprotocol): closingreason = ('PID protocol ' + data['pidprotocol'] + " does not fit " + 'PIS protocol ' + pisglobals.pisprotocol) # setting defined closing reasion for shutdown PID self.close(1011, closingreason) else: self.process_msg(data) except Exception as e: smsgwglobals.pislogger.debug("/ws: ERROR at WebSocektHandler " + "received_message: " + str(e))
def sendsms(self, **params): """ json before encoding { "smsid":"uuid.uuid1()", "modemid":"00436762222222", "targetnr":"+43200200200", "content":"test_sendsms200 ♠ ♣ ♥ ♦ ↙ ↺ ↻ ⇒ ä"} """ # for receiving sms from WIS cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) # smsgwglobals.pislogger.debug("/sendsms: rawbody: " + str(rawbody)) plaintext = GlobalHelper.decodeAES(rawbody) try: # smsgwglobals.pislogger.debug("/sendsms: plaintext: " + plaintext) data = json.loads(plaintext) # adding action switch for message to PID data['action'] = "sendsms" smsgwglobals.pislogger.debug("/sendsms: dictionary: " + str(data)) except Exception as e: smsgwglobals.pislogger.warning("/sendsms: Invalid data received! " + str(e)) cherrypy.response.status = 400 # Bad Request return try: address = PID.getclientaddress(data['modemid']) if address: # sending SMS to Pid PID.sendtopid(address, data) PID.addclientsms(address, data['smsid']) # Poll PIDsmstatus every 0.20 second till maxwait maxwaitpid = pisglobals.maxwaitpid now = datetime.utcnow() until = now + timedelta(seconds=maxwaitpid) while now < until: status = PID.getclientsmsstatus(address, data['smsid']) if status == 'SUCCESS': cherrypy.response.status = 200 PID.removeclientsms(address, data['smsid']) return if status == 'ERROR': cherrypy.response.status = 500 PID.removeclientsms(address, data['smsid']) return # wait for next run time.sleep(0.50) now = datetime.utcnow() # maxwaitpid reached so raise an error smsgwglobals.pislogger.warning("/sendsms: maxwaitpid " + "of " + str(maxwaitpid) + " seconds reached!") cherrypy.response.status = 500 PID.removeclientsms(address, data['smsid']) return else: smsgwglobals.pislogger.warning("/sendsms: No PID for " + "modem " + data['modemid'] + " found!") cherrypy.response.status = 500 # Internal Server Error return except Exception as e: smsgwglobals.pislogger.debug("/sendsms: Internal Server " "Error! " + str(e)) cherrypy.response.status = 500 # Internal Server Error return
def getsms(self, all=False, date=None): smsgwglobals.wislogger.debug("AJAX: " + str(all)) smsgwglobals.wislogger.debug("AJAX: " + str(date)) str_list = [] smsen = [] if all is False: smsgwglobals.wislogger.debug("AJAX: " + str(all)) try: if date is None: data = GlobalHelper.encodeAES('{"get": "sms"}') else: data = GlobalHelper.encodeAES('{"get": "sms", "date": "' + str(date) + '"}') if wisglobals.sslenabled is not None and 'true' in wisglobals.sslenabled.lower(): request = urllib.request.Request('https://' + wisglobals.wisipaddress + ':' + wisglobals.wisport + "/smsgateway/api/getsms") else: request = urllib.request.Request('http://' + wisglobals.wisipaddress + ':' + wisglobals.wisport + "/smsgateway/api/getsms") request.add_header("Content-Type", "application/json;charset=utf-8") f = urllib.request.urlopen(request, data, timeout=5) resp = f.read().decode('utf-8') respdata = GlobalHelper.decodeAES(resp) smsen = json.loads(respdata) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: getsms connect error") except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: getsms socket connection timeout") else: smsgwglobals.wislogger.debug("AJAX: " + str(all)) entries = wisglobals.rdb.read_wisurls_union() if len(entries) == 0: return "No Wis Urls" else: for entry in entries: try: if date is None: data = GlobalHelper.encodeAES('{"get": "sms"}') else: data = GlobalHelper.encodeAES('{"get": "sms", "date": "' + str(date) + '"}') request = urllib.request.Request(entry["wisurl"] + "/smsgateway/api/getsms") request.add_header("Content-Type", "application/json;charset=utf-8") f = urllib.request.urlopen(request, data, timeout=5) resp = f.read().decode('utf-8') respdata = GlobalHelper.decodeAES(resp) smsen = smsen + json.loads(respdata) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: getsms connect error") except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: getsms socket connection timeout") if smsen is None or len(smsen) == 0: return "No SMS in Tables found" th = [] tr = [] if len(smsen) > 0: od = collections.OrderedDict(sorted(smsen[0].items())) for k, v in od.items(): th.append(k) for sms in smsen: od = collections.OrderedDict(sorted(sms.items())) td = [] for k, v in od.items(): td.append(v) tr.append(td) str_list.append('<table id="smsTable" class="tablesorter">\n') str_list.append('<thead>\n') str_list.append('<tr>\n') for h in th: str_list.append('<th>' + h + '</th>\n') str_list.append('</tr>\n') str_list.append('</thead>\n') str_list.append('<tbody>\n') for r in tr: str_list.append('<tr>\n') for d in r: str_list.append('<td>' + str(d) + '</td>\n') str_list.append('</tr>') str_list.append('</tbody>\n') str_list.append('</table>\n') return ''.join(str_list)
def api(self, arg, **params): cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) smsgwglobals.wislogger.debug(rawbody) plaintext = GlobalHelper.decodeAES(rawbody) smsgwglobals.wislogger.debug(plaintext) data = json.loads(plaintext) if arg == "watchdog": if data["run"] == "True": self.triggerwatchdog() else: cherrypy.response.status = 400 if arg == "heartbeat": if "routingid" in data: smsgwglobals.wislogger.debug(data["routingid"]) try: count = wisglobals.rdb.raise_heartbeat(data["routingid"]) if count == 0: smsgwglobals.wislogger.debug("COUNT: " + str(count)) cherrypy.response.status = 400 except error.DatabaseError: cherrypy.response.status = 400 else: cherrypy.response.status = 400 if arg == "receiverouting": try: wisglobals.rdb.merge_routing(data) except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "requestrouting": if data["get"] != "peers": cherrypy.response.status = 400 return smsgwglobals.wislogger.debug("Sending routing table to you") try: erg = wisglobals.rdb.read_routing() jerg = json.dumps(erg) data = GlobalHelper.encodeAES(jerg) return data except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "managemodem": try: if data["action"] == "register": smsgwglobals.wislogger.debug("managemodem register") smsgwglobals.wislogger.debug(wisglobals.wisid) # add wisid to data object data["wisid"] = wisglobals.wisid # store date in routing table wisglobals.rdb.write_routing(data) # call receiverouting to distribute routing Helper.receiverouting() elif data["action"] == "unregister": smsgwglobals.wislogger.debug("managemodem unregister") routingid = data["routingid"] wisglobals.rdb.change_obsolete(routingid, 14) if routingid in wisglobals.watchdogRouteThread: wisglobals.watchdogRouteThread[routingid].terminate() wisglobals.watchdogRouteThread.pop(routingid) wisglobals.watchdogRouteThreadNotify.pop(routingid) wisglobals.watchdogRouteThreadQueue.pop(routingid) Helper.receiverouting() else: return False except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "deligatesms": if "sms" in data: smsgwglobals.wislogger.debug(data["sms"]) try: sms = Smstransfer(**data["sms"]) sms.smsdict["status"] = -1 sms.writetodb() self.triggerwatchdog() except error.DatabaseError: cherrypy.response.status = 400 else: cherrypy.response.status = 400 if arg == "router": if data["action"] == "status": smsgwglobals.wislogger.debug("API: " + data["action"]) if wisglobals.routerThread is None: cherrypy.response.status = 200 data = GlobalHelper.encodeAES('{"ROUTER":"noobject"}') return data if wisglobals.routerThread.isAlive(): cherrypy.response.status = 200 data = GlobalHelper.encodeAES('{"ROUTER":"alive"}') return data else: cherrypy.response.status = 200 data = GlobalHelper.encodeAES('{"ROUTER":"dead"}') return data if arg == "getsms": if data["get"] != "sms": cherrypy.response.status = 400 return if "date" in data: date = data["date"] smsgwglobals.wislogger.debug("API: " + date) else: date = None smsgwglobals.wislogger.debug("Sending SMS Table") smsgwglobals.wislogger.debug("Sending SMS Table date: " + str(date)) try: db = Database() erg = db.read_sms_date(date=date) jerg = json.dumps(erg) data = GlobalHelper.encodeAES(jerg) return data except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "get_sms_stats": if data["get"] != "sms": cherrypy.response.status = 400 return try: db = Database() erg = db.read_sms_stats() jerg = json.dumps(erg) data = GlobalHelper.encodeAES(jerg) return data except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message)
def sendsms(self, **params): """ json before encoding { "smsid":"uuid.uuid1()", "modemid":"00436762222222", "targetnr":"+43200200200", "content":"test_sendsms200 ♠ ♣ ♥ ♦ ↙ ↺ ↻ ⇒ ä"} """ # for receiving sms from WIS cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) # smsgwglobals.pislogger.debug("/sendsms: rawbody: " + str(rawbody)) plaintext = GlobalHelper.decodeAES(rawbody) try: # smsgwglobals.pislogger.debug("/sendsms: plaintext: " + plaintext) data = json.loads(plaintext) # adding action switch for message to PID data['action'] = "sendsms" smsgwglobals.pislogger.debug("/sendsms: dictionary: " + str(data)) except Exception as e: smsgwglobals.pislogger.warning( "/sendsms: Invalid data received! " + str(e)) cherrypy.response.status = 400 # Bad Request return try: address = PID.getclientaddress(data['modemid']) if address: # sending SMS to Pid PID.sendtopid(address, data) PID.addclientsms(address, data['smsid']) # Poll PIDsmstatus every 0.20 second till maxwait maxwaitpid = pisglobals.maxwaitpid now = datetime.utcnow() until = now + timedelta(seconds=maxwaitpid) while now < until: status, status_code = PID.getclientsmsstatus( address, data['smsid']) if status == 'SUCCESS' or status == "ERROR": cherrypy.response.status = 200 cherrypy.response.body = status_code PID.removeclientsms(address, data['smsid']) return str(status_code) # wait for next run time.sleep(0.50) now = datetime.utcnow() # maxwaitpid reached so raise an error smsgwglobals.pislogger.warning("/sendsms: maxwaitpid " + "of " + str(maxwaitpid) + " seconds reached!") # If timeout occured - set own status code status_code = 1000 cherrypy.response.status = 200 cherrypy.response.body = status_code PID.removeclientsms(address, data['smsid']) return str(status_code) else: smsgwglobals.pislogger.warning("/sendsms: No PID for " + "modem " + data['modemid'] + " found!") # If no modem endpoint to send - set own status code status_code = 2000 cherrypy.response.status = 200 cherrypy.response.body = status_code return str(status_code) except Exception as e: smsgwglobals.pislogger.debug("/sendsms: Internal Server " "Error! " + str(e)) cherrypy.response.status = 500 # Internal Server Error return
def api(self, arg, **params): cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) smsgwglobals.wislogger.debug(rawbody) plaintext = GlobalHelper.decodeAES(rawbody) smsgwglobals.wislogger.debug(plaintext) data = json.loads(plaintext) if arg == "watchdog": if data["run"] == "True": self.triggerwatchdog() else: cherrypy.response.status = 400 if arg == "heartbeat": if "routingid" in data: smsgwglobals.wislogger.debug(data["routingid"]) try: count = wisglobals.rdb.raise_heartbeat(data["routingid"]) if count == 0: smsgwglobals.wislogger.debug("COUNT: " + str(count)) cherrypy.response.status = 400 except error.DatabaseError: cherrypy.response.status = 400 else: cherrypy.response.status = 400 if arg == "receiverouting": try: wisglobals.rdb.merge_routing(data) except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "requestrouting": if data["get"] != "peers": cherrypy.response.status = 400 return smsgwglobals.wislogger.debug("Sending routing table to you") try: erg = wisglobals.rdb.read_routing() jerg = json.dumps(erg) data = GlobalHelper.encodeAES(jerg) return data except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "managemodem": try: if data["action"] == "register": smsgwglobals.wislogger.debug("managemodem register") smsgwglobals.wislogger.debug(wisglobals.wisid) # add wisid to data object data["wisid"] = wisglobals.wisid # store date in routing table wisglobals.rdb.write_routing(data) # call receiverouting to distribute routing Helper.receiverouting() elif data["action"] == "unregister": smsgwglobals.wislogger.debug("managemodem unregister") wisglobals.rdb.change_obsolete(data["routingid"], 14) Helper.receiverouting() else: return False except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message) if arg == "deligatesms": if "sms" in data: smsgwglobals.wislogger.debug(data["sms"]) try: sms = Smstransfer(**data["sms"]) sms.smsdict["status"] = 1 sms.writetodb() self.triggerwatchdog() except error.DatabaseError: cherrypy.response.status = 400 else: cherrypy.response.status = 400 if arg == "router": if data["action"] == "status": smsgwglobals.wislogger.debug("API: " + data["action"]) if wisglobals.routerThread is None: cherrypy.response.status = 200 data = GlobalHelper.encodeAES('{"ROUTER":"noobject"}') return data if wisglobals.routerThread.isAlive(): cherrypy.response.status = 200 data = GlobalHelper.encodeAES('{"ROUTER":"alive"}') return data else: cherrypy.response.status = 200 data = GlobalHelper.encodeAES('{"ROUTER":"dead"}') return data if arg == "getsms": if data["get"] != "sms": cherrypy.response.status = 400 return if "date" in data: date = data["date"] smsgwglobals.wislogger.debug("API: " + date) else: date = None smsgwglobals.wislogger.debug("Sending SMS Table") smsgwglobals.wislogger.debug("Sending SMS Table date: " + str(date)) try: db = Database() erg = db.read_sms_date(date=date) jerg = json.dumps(erg) data = GlobalHelper.encodeAES(jerg) return data except error.DatabaseError as e: smsgwglobals.wislogger.debug(e.message)
def getsms(self, all=False, date=None): smsgwglobals.wislogger.debug("AJAX: " + str(all)) smsgwglobals.wislogger.debug("AJAX: " + str(date)) str_list = [] smsen = [] if all is False: smsgwglobals.wislogger.debug("AJAX: " + str(all)) try: if date is None: data = GlobalHelper.encodeAES('{"get": "sms"}') else: data = GlobalHelper.encodeAES('{"get": "sms", "date": "' + str(date) + '"}') if wisglobals.sslenabled is not None and 'true' in wisglobals.sslenabled.lower( ): request = urllib.request.Request('https://' + wisglobals.wisipaddress + ':' + wisglobals.wisport + "/smsgateway/api/getsms") else: request = urllib.request.Request('http://' + wisglobals.wisipaddress + ':' + wisglobals.wisport + "/smsgateway/api/getsms") request.add_header("Content-Type", "application/json;charset=utf-8") f = urllib.request.urlopen(request, data, timeout=5) resp = f.read().decode('utf-8') respdata = GlobalHelper.decodeAES(resp) smsen = json.loads(respdata) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug("AJAX: getsms connect error") except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug( "AJAX: getsms socket connection timeout") else: smsgwglobals.wislogger.debug("AJAX: " + str(all)) entries = wisglobals.rdb.read_wisurls_union() if len(entries) == 0: return "No Wis Urls" else: for entry in entries: try: if date is None: data = GlobalHelper.encodeAES('{"get": "sms"}') else: data = GlobalHelper.encodeAES( '{"get": "sms", "date": "' + str(date) + '"}') request = urllib.request.Request( entry["wisurl"] + "/smsgateway/api/getsms") request.add_header("Content-Type", "application/json;charset=utf-8") f = urllib.request.urlopen(request, data, timeout=5) resp = f.read().decode('utf-8') respdata = GlobalHelper.decodeAES(resp) smsen = smsen + json.loads(respdata) except urllib.error.URLError as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug( "AJAX: getsms connect error") except socket.timeout as e: smsgwglobals.wislogger.debug(e) smsgwglobals.wislogger.debug( "AJAX: getsms socket connection timeout") if smsen is None or len(smsen) == 0: return "No SMS in Tables found" th = [] tr = [] if len(smsen) > 0: od = collections.OrderedDict(sorted(smsen[0].items())) for k, v in od.items(): th.append(k) for sms in smsen: od = collections.OrderedDict(sorted(sms.items())) td = [] for k, v in od.items(): td.append(v) tr.append(td) str_list.append('<table id="smsTable" class="tablesorter">\n') str_list.append('<thead>\n') str_list.append('<tr>\n') for h in th: str_list.append('<th>' + h + '</th>\n') str_list.append('</tr>\n') str_list.append('</thead>\n') str_list.append('<tbody>\n') for r in tr: str_list.append('<tr>\n') for d in r: str_list.append('<td>' + str(d) + '</td>\n') str_list.append('</tr>') str_list.append('</tbody>\n') str_list.append('</table>\n') return ''.join(str_list)