def get_ready_to_run(self): (ready, stamps, finals) = self.scan_ready_to_run() if ready: for info in ready: info.stamp = stamps[info.basename] with eagn.DB() as db: invalids = set() for mailing in (agn.Stream(ready).map( lambda i: i.mailing).distinct().sorted()): rq = db.querys( 'SELECT deleted ' 'FROM mailing_tbl ' 'WHERE mailing_id = :mailingID', {'mailingID': mailing}) if rq is None: agn.log(agn.LV_INFO, 'ready', 'Mailing %d no more existing' % mailing) invalids.add(mailing) elif rq.deleted: agn.log(agn.LV_INFO, 'ready', 'Mailing %d is marked as deleted' % mailing) invalids.add(mailing) if invalids: for info in (agn.Stream(ready).filter( lambda i: i.mailing in invalids)): self.move(info.path, self.deleted) self.move(info.stamp.path, self.deleted) ready = (agn.Stream(ready).filter( lambda i: i.mailing not in invalids).list()) if ready: agn.log(agn.LV_INFO, 'ready', '%s files are ready to send' % agn.numfmt(len(ready))) return ready
def processCompleted(self): max_count = 25000 # max # of records to delete per batch outdated = 24 * 60 * 60 # if record reached this value, the record is assumed to be done to_remove = [] agn.log(agn.LV_DEBUG, 'proc', 'Start processing completed records') for (section, key, value) in self.mtrack: diff = int(time.time()) - value[self.mtrack.key_created] if diff < 3600: diffstr = '%d:%02d' % (diff // 60, diff % 60) else: diffstr = '%d:%02d:%02d' % (diff // 3600, (diff // 60) % 60, diff % 60) # if section != self.SEC_MTAID: agn.log( agn.LV_DEBUG, 'proc', 'Ignore non %s record: %s:%s' % (self.SEC_MTAID, section, key)) continue # if not value.get('complete'): if diff < outdated: agn.log( agn.LV_DEBUG, 'proc/%s' % key, 'Ignore not (yet) completed record since %s' % diffstr) continue agn.log(agn.LV_INFO, 'proc/%s' % key, 'Found outdated incomplete record since %s' % diffstr) value['outdated'] = True # self.completed(key, value, to_remove) # to_remove.append((section, key)) if len(to_remove) >= max_count: agn.log( agn.LV_INFO, 'proc', 'Reached limit of %s, defer further processing' % agn.numfmt(max_count)) break # if to_remove: agn.log(agn.LV_VERBOSE, 'proc', 'Remove %s processed keys' % agn.numfmt(len(to_remove))) for (section, key) in to_remove: self.mtrack.delete(section, key) agn.log(agn.LV_DEBUG, 'proc', 'Removed %s:%s' % (section, key)) agn.log(agn.LV_VERBOSE, 'proc', 'Removed processed keys done')
def collectNewBounces(self): #{{{ agn.log(agn.LV_INFO, 'collect', 'Start collecting new bounces') iquery = 'INSERT INTO bounce_collect_tbl (customer_id, company_id, mailing_id, change_date) VALUES (:customer, :company, :mailing, current_timestamp)' insert = self.db.cursor() if insert is None: raise agn.error( 'collectNewBounces: Failed to get new cursor for insertion') bquery = self.db.cursor() if bquery is None: raise agn.error( 'collectNewBounces: Failed to get new cursor for bounce query') data = {} query = 'SELECT customer_id, company_id, mailing_id, detail FROM bounce_tbl WHERE %s ORDER BY company_id, customer_id' % self.timestamp.makeBetweenClause( 'change_date', data) cur = [0, 0, 0, 0] (records, uniques, inserts) = (0, 0, 0) if bquery.query(query, data) is None: raise agn.error( 'collectNewBounces: Failed to query bounce_tbl using: %s' % query) while cur is not None: try: record = bquery.next() records += 1 except StopIteration: record = None if record is None or cur[0] != record[0] or cur[1] != record[1]: if record is not None: uniques += 1 if cur[0] > 0 and cur[3] >= 400 and cur[3] < 510: parm = { 'customer': cur[0], 'company': cur[1], 'mailing': cur[2] } insert.update(iquery, parm) inserts += 1 if inserts % 10000 == 0 or record is None: agn.log( agn.LV_DEBUG, 'collect', 'Inserted now %s record%s' % (agn.numfmt(inserts), exts(inserts))) insert.sync() cur = record elif record[3] > cur[3]: cur = list(record) self.db.commit() bquery.close() insert.close() agn.log( agn.LV_INFO, 'collect', 'Read %d records (%d uniques) and inserted %d' % (records, uniques, inserts))
def collectNewBounces (self): #{{{ agn.log (agn.LV_INFO, 'collect', 'Start collecting new bounces') iquery = 'INSERT INTO bounce_collect_tbl (customer_id, company_id, mailing_id, change_date) VALUES (:customer, :company, :mailing, current_timestamp)' insert = self.db.cursor () if insert is None: raise agn.error ('collectNewBounces: Failed to get new cursor for insertion') bquery = self.db.cursor () if bquery is None: raise agn.error ('collectNewBounces: Failed to get new cursor for bounce query') data = {} query = 'SELECT customer_id, company_id, mailing_id, detail FROM bounce_tbl WHERE %s ORDER BY company_id, customer_id' % self.timestamp.makeBetweenClause ('change_date', data) cur = [0, 0, 0, 0] (records, uniques, inserts) = (0, 0, 0) if bquery.query (query, data) is None: raise agn.error ('collectNewBounces: Failed to query bounce_tbl using: %s' % query) while cur is not None: try: record = bquery.next () records += 1 except StopIteration: record = None if record is None or cur[0] != record[0] or cur[1] != record[1]: if record is not None: uniques += 1 if cur[0] > 0 and cur[3] >= 400 and cur[3] < 510: parm = { 'customer': cur[0], 'company': cur[1], 'mailing': cur[2] } insert.update (iquery, parm) inserts += 1 if inserts % 10000 == 0 or record is None: agn.log (agn.LV_DEBUG, 'collect', 'Inserted now %s record%s' % (agn.numfmt (inserts), exts (inserts))) insert.sync () cur = record elif record[3] > cur[3]: cur = list (record) self.db.commit () bquery.close () insert.close () agn.log (agn.LV_INFO, 'collect', 'Read %d records (%d uniques) and inserted %d' % (records, uniques, inserts))
return False try: fd = open (tfname, 'r') except IOError, e: agn.log (agn.LV_ERROR, 'update', 'Unable to open %s: %s' % (tfname, `e.args`)) fd = None if fd is None: return False self.lineno = 0 removeTemp = True rc = self.updateStart (inst) for line in [agn.chop (l) for l in fd.readlines ()]: self.lineno += 1 if self.lineno % 10000 == 0: agn.log (agn.LV_INFO, 'update', '%s: Now at line %s' % (self.name, agn.numfmt (self.lineno))) if not self.updateLine (inst, line): if not self.saveToFail (line): removeTemp = False rc = False else: if not self.saveToLog (line): removeTemp = False if not self.updateEnd (inst): rc = False fd.close () if removeTemp: self.__removeFile (tfname) inst.sync () return rc
except IOError, e: agn.log(agn.LV_ERROR, 'update', 'Unable to open %s: %s' % (tfname, ` e.args `)) fd = None if fd is None: return False self.lineno = 0 removeTemp = True rc = self.updateStart(inst) for line in [agn.chop(l) for l in fd.readlines()]: self.lineno += 1 if self.lineno % 10000 == 0: agn.log( agn.LV_INFO, 'update', '%s: Now at line %s' % (self.name, agn.numfmt(self.lineno))) if not self.updateLine(inst, line): if not self.saveToFail(line): removeTemp = False rc = False else: if not self.saveToLog(line): removeTemp = False if not self.updateEnd(inst): rc = False fd.close() if removeTemp: self.__removeFile(tfname) inst.sync() return rc
def convertToHardbounce(self): #{{{ agn.log(agn.LV_INFO, 'conv', 'Start converting softbounces to hardbounce') coll = [1] stats = [] for company in sorted(coll): cstat = [company, 0, 0] stats.append(cstat) agn.log(agn.LV_INFO, 'conv', 'Working on %d' % company) dquery = 'DELETE FROM softbounce_email_tbl WHERE company_id = %d AND email = :email' % company dcurs = self.db.cursor() uquery = self.curs.rselect( 'UPDATE customer_%d_binding_tbl SET user_status = 2, user_remark = \'Softbounce\', exit_mailing_id = :mailing, change_date = %%(sysdate)s WHERE customer_id = :customer AND user_status = 1' % company) bquery = self.curs.rselect( 'INSERT INTO bounce_tbl (company_id, customer_id, detail, mailing_id, change_date, dsn) VALUES (%d, :customer, 510, :mailing, %%(sysdate)s, 599)' % company) ucurs = self.db.cursor() squery = 'SELECT email, mailing_id, bnccnt, creation_date, change_date FROM softbounce_email_tbl WHERE company_id = %d AND bnccnt > 7 AND DATEDIFF(change_date,creation_date) > 30' % company scurs = self.db.cursor() if None in [dcurs, ucurs, scurs]: raise agn.error('Failed to setup curses') lastClick = 30 lastOpen = 30 def toDatetime(offset): tm = time.localtime(time.time() - offset * 24 * 60 * 60) return datetime.datetime(tm.tm_year, tm.tm_mon, tm.tm_mday) lastClickTS = toDatetime(lastClick) lastOpenTS = toDatetime(lastOpen) ccount = 0 for record in scurs.query(squery): parm = { 'email': record[0], 'mailing': record[1], 'bouncecount': record[2], 'creationdate': record[3], 'timestamp': record[4], 'customer': None } query = 'SELECT customer_id FROM customer_%d_tbl WHERE email = :email ' % company data = self.curs.querys(query, parm, cleanup=True) if data is None: continue custs = [ agn.struct(id=_d, click=0, open=0) for _d in data if _d ] if not custs: continue if len(custs) == 1: cclause = 'customer_id = %d' % custs[0].id else: cclause = 'customer_id IN (%s)' % ', '.join( [str(_c.id) for _c in custs]) parm['ts'] = lastClickTS query = 'SELECT customer_id, count(*) FROM rdir_log_tbl WHERE %s AND company_id = %d' % ( cclause, company) query += ' AND change_date > :ts GROUP BY customer_id' for r in self.curs.queryc(query, parm, cleanup=True): for c in custs: if c.id == r[0]: c.click += r[1] parm['ts'] = lastOpenTS query = 'SELECT customer_id, count(*) FROM onepixel_log_tbl WHERE %s AND company_id = %d' % ( cclause, company) query += ' AND change_date > :ts GROUP BY customer_id' for r in self.curs.queryc(query, parm, cleanup=True): for c in custs: if c.id == r[0]: c.open += r[1] for c in custs: if c.click > 0 or c.open > 0: cstat[1] += 1 agn.log( agn.LV_INFO, 'conv', 'Email %s [%d] has %d klick(s) and %d onepix(es) -> active' % (parm['email'], c.id, c.click, c.open)) else: cstat[2] += 1 agn.log( agn.LV_INFO, 'conv', 'Email %s [%d] has no klicks and no onepixes -> hardbounce' % (parm['email'], c.id)) parm['customer'] = c.id ucurs.update(uquery, parm, cleanup=True) ucurs.execute(bquery, parm, cleanup=True) dcurs.update(dquery, parm, cleanup=True) ccount += 1 if ccount % 1000 == 0: agn.log(agn.LV_INFO, 'conv', 'Commiting at %s' % agn.numfmt(ccount)) self.db.commit() self.db.commit() scurs.close() ucurs.close() dcurs.close() for cstat in stats: agn.log( agn.LV_INFO, 'conv', 'Company %d has %d active and %d marked as hardbounced users' % tuple(cstat)) agn.log(agn.LV_INFO, 'conv', 'Converting softbounces to hardbounce done')
def convertToHardbounce(self): #{{{ agn.log(agn.LV_INFO, 'conv', 'Start converting softbounces to hardbounce') coll = self.__ccoll( 'SELECT distinct company_id FROM softbounce_email_tbl WHERE company_id IN (SELECT company_id FROM company_tbl WHERE status = \'active\')', None, None, None) stats = [] for company in sorted(coll): cstat = [company, 0, 0] stats.append(cstat) agn.log(agn.LV_INFO, 'conv', 'Working on %d' % company) dquery = 'DELETE FROM softbounce_email_tbl WHERE company_id = %d AND email = :email' % company dcurs = self.db.cursor() uquery = self.curs.rselect( 'UPDATE customer_%d_binding_tbl SET user_status = 2, user_remark = \'bounce:soft\', exit_mailing_id = :mailing, timestamp = %%(sysdate)s WHERE customer_id = :customer AND user_status = 1' % company) bquery = self.curs.rselect( 'INSERT INTO bounce_tbl (company_id, customer_id, detail, mailing_id, timestamp, dsn) VALUES (%d, :customer, 510, :mailing, %%(sysdate)s, 599)' % company) ucurs = self.db.cursor() squery = 'SELECT email, mailing_id, bnccnt, creation_date, timestamp FROM softbounce_email_tbl ' bnccnt = self.__cfg(company, 'convert-bounce-count', 40) daydiff = self.__cfg(company, 'convert-bounce-duration', 30) squery += self.curs.qselect( oracle= 'WHERE company_id = %d AND bnccnt > %d AND timestamp-creation_date > %d' % (company, bnccnt, daydiff), mysql= 'WHERE company_id = %d AND bnccnt > %d AND DATEDIFF(timestamp,creation_date) > %d' % (company, bnccnt, daydiff)) scurs = self.db.cursor() if None in [dcurs, ucurs, scurs]: raise agn.error('Failed to setup curses') lastClick = self.__cfg(company, 'last-click', 30) lastOpen = self.__cfg(company, 'last-open', 30) def toDatetime(offset): tm = time.localtime(time.time() - offset * 24 * 60 * 60) return datetime.datetime(tm.tm_year, tm.tm_mon, tm.tm_mday) lastClickTS = toDatetime(lastClick) lastOpenTS = toDatetime(lastOpen) if Cache is not None: rcache = Cache(1000) ocache = Cache(1000 * 1000) ccurs = self.db.cursor() if None in [rcache, ocache, ccurs ] or not rcache.valid() or not ocache.valid(): raise agn.error('Failed to setup caching') agn.log(agn.LV_INFO, 'cache', 'Setup rdir log cache for %d' % company) query = 'SELECT customer_id FROM rdirlog_%d_tbl WHERE timestamp > :ts' % company parm = {'ts': lastClickTS} for record in ccurs.query(query, parm): rcache.add(record[0]) agn.log(agn.LV_INFO, 'cache', 'Setup one pixel log cache for %d' % company) query = 'SELECT customer_id FROM onepixellog_%d_tbl WHERE timestamp > :ts' % company parm = {'ts': lastOpenTS} for record in ccurs.query(query, parm): ocache.add(record[0]) ccurs.close() agn.log(agn.LV_INFO, 'cache', 'Setup completed') ccount = 0 for record in scurs.query(squery): parm = { 'email': record[0], 'mailing': record[1], 'bouncecount': record[2], 'creationdate': record[3], 'timestamp': record[4], 'customer': None } query = 'SELECT customer_id FROM customer_%d_tbl WHERE email = :email ' % company data = self.curs.querys(query, parm, cleanup=True) if data is None: continue custs = [ agn.mutable(id=_d, click=0, open=0) for _d in data if _d ] if not custs: continue if len(custs) == 1: cclause = 'customer_id = %d' % custs[0].id else: cclause = 'customer_id IN (%s)' % ', '.join( [str(_c.id) for _c in custs]) if Cache is not None: for c in custs: c.click += rcache.get(c.id, 0) c.open += ocache.get(c.id, 0) else: parm['ts'] = lastClickTS query = 'SELECT customer_id, count(*) FROM rdirlog_%d_tbl WHERE %s AND timestamp > :ts GROUP BY customer_id' % ( company, cclause) for r in self.curs.queryc(query, parm, cleanup=True): for c in custs: if c.id == r[0]: c.click += r[1] parm['ts'] = lastOpenTS query = 'SELECT customer_id, count(*) FROM onepixellog_%d_tbl WHERE %s AND timestamp > :ts GROUP BY customer_id' % ( company, cclause) for r in self.curs.queryc(query, parm, cleanup=True): for c in custs: if c.id == r[0]: c.open += r[1] for c in custs: if c.click > 0 or c.open > 0: cstat[1] += 1 agn.log( agn.LV_INFO, 'conv', 'Email %s [%d] has %d klick(s) and %d onepix(es) -> active' % (parm['email'], c.id, c.click, c.open)) else: cstat[2] += 1 agn.log( agn.LV_INFO, 'conv', 'Email %s [%d] has no klicks and no onepixes -> hardbounce' % (parm['email'], c.id)) parm['customer'] = c.id ucurs.update(uquery, parm, cleanup=True) ucurs.execute(bquery, parm, cleanup=True) dcurs.update(dquery, parm, cleanup=True) ccount += 1 if ccount % 1000 == 0: agn.log(agn.LV_INFO, 'conv', 'Commiting at %s' % agn.numfmt(ccount)) self.db.commit() if Cache is not None: rcache.done() ocache.done() self.db.commit() scurs.close() ucurs.close() dcurs.close() for cstat in stats: agn.log( agn.LV_INFO, 'conv', 'Company %d has %d active and %d marked as hardbounced users' % tuple(cstat)) agn.log(agn.LV_INFO, 'conv', 'Converting softbounces to hardbounce done')
def collectNewBounces(self): #{{{ agn.log(agn.LV_INFO, 'collect', 'Start collecting new bounces') cursor = self.db.cursor() if cursor is None: raise agn.error( 'collectNewBounces: Failed to get new cursor for collecting') # Update = collections.namedtuple( 'Update', ['customer_id', 'company_id', 'mailing_id', 'detail']) data = {} query = 'SELECT customer_id, company_id, mailing_id, detail FROM bounce_tbl WHERE %s ORDER BY company_id, customer_id' % self.timestamp.makeBetweenClause( 'timestamp', data) class Collect(agn.Stream.Collector): def supplier(self): self.data = {} self.uniques = 0 def accumulator(self, supplier, element): update = Update(*element) if update.detail >= 400 and update.detail < 520: key = (update.company_id, update.customer_id) try: if update.detail > self.data[key].detail: self.data[key] = update except KeyError: self.data[key] = update self.uniques += 1 def finisher(self, supplier, count): return (count, self.uniques, self.data) (records, uniques, updates) = cursor.stream(query, data).collect(Collect()) agn.log( agn.LV_INFO, 'collect', 'Read %s records (%s uniques) and have %s for insert' % (agn.numfmt(records), agn.numfmt(uniques), agn.numfmt( len(updates)))) # inserts = 0 query = self.curs.rselect( 'INSERT INTO bounce_collect_tbl (customer_id, company_id, mailing_id, timestamp) VALUES (:customer, :company, :mailing, %(sysdate)s)' ) for update in (agn.Stream(updates.itervalues()).sorted()): cursor.update( query, { 'customer': update.customer_id, 'company': update.company_id, 'mailing': update.mailing_id }) inserts += 1 if inserts % 10000 == 0: cursor.sync() agn.log( agn.LV_INFO, 'collect', 'Inserted %s records into bounce_collect_tbl' % agn.numfmt(inserts)) cursor.sync() cursor.close() # agn.log( agn.LV_INFO, 'collect', 'Read %d records (%d uniques) and inserted %d' % (records, uniques, inserts)) companyIDs = [] query = 'SELECT distinct company_id FROM bounce_collect_tbl' for record in self.curs.query(query): if record[0] is not None and record[0] > 0: companyIDs.append(record[0]) agn.log( agn.LV_INFO, 'collect', 'Remove active receivers from being watched for %d compan%s' % (len(companyIDs), exty(len(companyIDs)))) allCount = 0 for companyID in companyIDs: table = 'success_%d_tbl' % companyID if table in self.tables: data = {'companyID': companyID} query = self.curs.qselect ( oracle = 'DELETE FROM bounce_collect_tbl mail WHERE EXISTS (SELECT 1 FROM %s su WHERE ' % table + \ 'mail.customer_id = su.customer_id AND mail.company_id = :companyID AND %s)' % self.timestamp.makeBetweenClause ('timestamp', data), mysql = 'DELETE mail.* FROM bounce_collect_tbl mail, %s su WHERE ' % table + \ 'mail.customer_id = su.customer_id AND mail.company_id = :companyID AND %s' % self.timestamp.makeBetweenClause ('su.timestamp', data) ) count = self.curs.execute(query, data, commit=True) agn.log( agn.LV_INFO, 'collect', 'Removed %s active receiver%s for companyID %d' % (agn.numfmt(count), exts(count), companyID)) allCount += count else: agn.log( agn.LV_INFO, 'collect', 'Skip removing active receivers for companyID %d due to missing table %s' % (companyID, table)) agn.log( agn.LV_INFO, 'collect', 'Finished removing %s receiver%s' % (agn.numfmt(allCount), exts(allCount)))
def convertToHardbounce (self): #{{{ agn.log (agn.LV_INFO, 'conv', 'Start converting softbounces to hardbounce') coll = [1] stats = [] for company in sorted (coll): cstat = [company, 0, 0] stats.append (cstat) agn.log (agn.LV_INFO, 'conv', 'Working on %d' % company) dquery = 'DELETE FROM softbounce_email_tbl WHERE company_id = %d AND email = :email' % company dcurs = self.db.cursor () uquery = self.curs.rselect ('UPDATE customer_%d_binding_tbl SET user_status = 2, user_remark = \'Softbounce\', exit_mailing_id = :mailing, change_date = %%(sysdate)s WHERE customer_id = :customer AND user_status = 1' % company) bquery = self.curs.rselect ('INSERT INTO bounce_tbl (company_id, customer_id, detail, mailing_id, change_date, dsn) VALUES (%d, :customer, 510, :mailing, %%(sysdate)s, 599)' % company) ucurs = self.db.cursor () squery = 'SELECT email, mailing_id, bnccnt, creation_date, change_date FROM softbounce_email_tbl WHERE company_id = %d AND bnccnt > 7 AND DATEDIFF(change_date,creation_date) > 30' % company scurs = self.db.cursor () if None in [dcurs, ucurs, scurs]: raise agn.error ('Failed to setup curses') lastClick = 30 lastOpen = 30 def toDatetime (offset): tm = time.localtime (time.time () -offset * 24 * 60 * 60) return datetime.datetime (tm.tm_year, tm.tm_mon, tm.tm_mday) lastClickTS = toDatetime (lastClick) lastOpenTS = toDatetime (lastOpen) ccount = 0 for record in scurs.query (squery): parm = { 'email': record[0], 'mailing': record[1], 'bouncecount': record[2], 'creationdate': record[3], 'timestamp': record[4], 'customer': None } query = 'SELECT customer_id FROM customer_%d_tbl WHERE email = :email ' % company data = self.curs.querys (query, parm, cleanup = True) if data is None: continue custs = [agn.struct (id = _d, click = 0, open = 0) for _d in data if _d] if not custs: continue if len (custs) == 1: cclause = 'customer_id = %d' % custs[0].id else: cclause = 'customer_id IN (%s)' % ', '.join ([str (_c.id) for _c in custs]) parm['ts'] = lastClickTS query = 'SELECT customer_id, count(*) FROM rdir_log_tbl WHERE %s AND company_id = %d' % (cclause, company) query += ' AND change_date > :ts GROUP BY customer_id' for r in self.curs.queryc (query, parm, cleanup = True): for c in custs: if c.id == r[0]: c.click += r[1] parm['ts'] = lastOpenTS query = 'SELECT customer_id, count(*) FROM onepixel_log_tbl WHERE %s AND company_id = %d' % (cclause, company) query += ' AND change_date > :ts GROUP BY customer_id' for r in self.curs.queryc (query, parm, cleanup = True): for c in custs: if c.id == r[0]: c.open += r[1] for c in custs: if c.click > 0 or c.open > 0: cstat[1] += 1 agn.log (agn.LV_INFO, 'conv', 'Email %s [%d] has %d klick(s) and %d onepix(es) -> active' % (parm['email'], c.id, c.click, c.open)) else: cstat[2] += 1 agn.log (agn.LV_INFO, 'conv', 'Email %s [%d] has no klicks and no onepixes -> hardbounce' % (parm['email'], c.id)) parm['customer'] = c.id ucurs.update (uquery, parm, cleanup = True) ucurs.execute (bquery, parm, cleanup = True) dcurs.update (dquery, parm, cleanup = True) ccount += 1 if ccount % 1000 == 0: agn.log (agn.LV_INFO, 'conv', 'Commiting at %s' % agn.numfmt (ccount)) self.db.commit () self.db.commit () scurs.close () ucurs.close () dcurs.close () for cstat in stats: agn.log (agn.LV_INFO, 'conv', 'Company %d has %d active and %d marked as hardbounced users' % tuple (cstat)) agn.log (agn.LV_INFO, 'conv', 'Converting softbounces to hardbounce done')