def post(self, request): bot = Bot() try: bot.incoming(request.body) except Exception as e: bot.logger.critical(e) return HttpResponse('ok')
class Command(BaseCommand): help = 'Send out all unsent tasks' TELEGRAM_BOT = Bot() def add_arguments(self, parser): parser.add_argument('--coders', nargs='+') parser.add_argument('--dryrun', action='store_true', default=False) def get_message(self, task): if 'contests' in task.addition: notify = task.notification contests = Contest.objects.filter(pk__in=task.addition['contests']) context = deepcopy(task.addition.get('context', {})) context.update({ 'contests': contests, 'notification': notify, 'coder': notify.coder, 'domain': settings.HTTPS_HOST_, }) subject = render_to_string('subject', context).strip() context['subject'] = subject method = notify.method.split(':', 1)[0] message = render_to_string('message/%s' % method, context).strip() else: subject = '' message = '' context = {} if task.subject: subject = task.subject + subject if task.message: message = task.message + message return subject, message, context @print_sql_decorator() @transaction.atomic def handle(self, *args, **options): coders = options.get('coders') dryrun = options.get('dryrun') logger = getLogger('notification.sendout.tasks') delete_info = Task.objects.filter(is_sent=True, modified__lte=now() - timedelta(days=31)).delete() logger.info(f'Tasks cleared: {delete_info}') if dryrun: qs = Task.objects.all() else: qs = Task.unsent.all() qs = qs.select_related('notification__coder') qs = qs.prefetch_related( Prefetch( 'notification__coder__chat_set', queryset=Chat.objects.filter(is_group=False), to_attr='cchat', )) if coders: qs = qs.filter(notification__coder__username__in=coders) if dryrun: qs = qs.order_by('-modified')[:1] done = 0 failed = 0 for task in tqdm.tqdm(qs.iterator(), 'sending'): try: task.is_sent = True notification = task.notification coder = notification.coder method, *args = notification.method.split(':', 1) message = self.get_message(task) subject, message, context = self.get_message(task) if method == Notification.TELEGRAM: if args: self.TELEGRAM_BOT.send_message(message, args[0], reply_markup=False) elif coder.chat and coder.chat.chat_id: try: if not coder.settings.get('telegram', {}).get( 'unauthorized', False): self.TELEGRAM_BOT.send_message( message, coder.chat.chat_id, reply_markup=False) except Unauthorized: coder.settings.setdefault( 'telegram', {})['unauthorized'] = True coder.save() elif method == Notification.EMAIL: send_mail( subject, message, 'CLIST <*****@*****.**>', [coder.user.email], fail_silently=False, html_message=message, ) elif method == Notification.WEBBROWSER: payload = { 'head': subject, 'body': message, } contests = list(context.get('contests', [])) if len(contests) == 1: contest = contests[0] payload['url'] = contest.url payload[ 'icon'] = f'{settings.HTTPS_HOST_}/imagefit/static_resize/64x64/{contest.resource.icon}' send_user_notification( user=coder.user, payload=payload, ttl=300, ) except Exception: logger.error('Exception sendout task:\n%s' % format_exc()) task.is_sent = False if task.is_sent: done += 1 else: failed += 1 task.save() logger.info(f'Done: {done}, failed: {failed}')
class Command(BaseCommand): help = 'Send out all unsent tasks' TELEGRAM_BOT = Bot() CONFIG_FILE = __file__ + '.yaml' N_STOP_EMAIL_FAILED_LIMIT = 5 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.email_connection = None self.n_messages_sent = 0 self.config = None def add_arguments(self, parser): parser.add_argument('--coders', nargs='+') parser.add_argument('--dryrun', action='store_true', default=False) def get_message(self, method, data, **kwargs): subject_ = kwargs.pop('subject', None) message_ = kwargs.pop('message', None) if 'contests' in data: contests = Contest.objects.filter(pk__in=data['contests']) context = deepcopy(data.get('context', {})) context.update({ 'contests': contests, 'domain': settings.MAIN_HOST_, }) context.update(kwargs) subject = render_to_string('subject', context).strip() subject = re.sub(r'\s+', ' ', subject) context['subject'] = subject method = method.split(':', 1)[0] message = render_to_string('message/%s' % method, context).strip() else: subject = '' message = '' context = {} if subject_: subject = subject_ + subject if message_: message = message_ + message return subject, message, context def send_message(self, coder, method, data, **kwargs): method, *args = method.split(':', 1) subject, message, context = self.get_message(method=method, data=data, coder=coder, **kwargs) if method == settings.NOTIFICATION_CONF.TELEGRAM: if args: try: self.TELEGRAM_BOT.send_message(message, args[0], reply_markup=False) except Unauthorized as e: if 'bot was kicked from' in str(e): if 'notification' in kwargs: delete_info = kwargs['notification'].delete() logger.error( f'{str(e)}, delete info = {delete_info}') return 'removed' elif coder.chat and coder.chat.chat_id: try: if not coder.settings.get('telegram', {}).get( 'unauthorized', False): self.TELEGRAM_BOT.send_message(message, coder.chat.chat_id, reply_markup=False) except Unauthorized as e: if 'bot was blocked by the user' in str(e): coder.chat.delete() else: coder.settings.setdefault('telegram', {})['unauthorized'] = True coder.save() elif 'notification' in kwargs: delete_info = kwargs['notification'].delete() logger.error( f'Strange notification, delete info = {delete_info}') return 'removed' elif method == settings.NOTIFICATION_CONF.EMAIL: if self.n_messages_sent % 20 == 0: if self.n_messages_sent: sleep(10) self.email_connection = EmailBackend() mail = EmailMultiAlternatives( subject=subject, body=message, from_email='CLIST <*****@*****.**>', to=[coder.user.email], bcc=['*****@*****.**'], connection=self.email_connection, alternatives=[(message, 'text/html')], ) mail.send() self.n_messages_sent += 1 sleep(2) elif method == settings.NOTIFICATION_CONF.WEBBROWSER: payload = { 'head': subject, 'body': message, } contests = list(context.get('contests', [])) if len(contests) == 1: contest = contests[0] payload['url'] = contest.url payload[ 'icon'] = f'{settings.HTTPS_HOST_}/imagefit/static_resize/64x64/{contest.resource.icon}' try: send_user_notification( user=coder.user, payload=payload, ttl=300, ) except WebPushException as e: if '403 Forbidden' in str(e): if 'notification' in kwargs: delete_info = kwargs['notification'].delete() logger.error(f'{str(e)}, delete info = {delete_info}') return 'removed' def load_config(self): if os.path.exists(self.CONFIG_FILE): with open(self.CONFIG_FILE, 'r') as fo: self.config = yaml.safe_load(fo) else: self.config = {} self.config.setdefault('stop_email', {}) self.config['stop_email'].setdefault('n_failed', 0) def save_config(self): lock = FileLock(self.CONFIG_FILE) with lock.acquire(timeout=60): with open(self.CONFIG_FILE, 'w') as fo: yaml.dump(self.config, fo, indent=2) @print_sql_decorator() def handle(self, *args, **options): self.load_config() coders = options.get('coders') dryrun = options.get('dryrun') stop_email = settings.STOP_EMAIL_ and not dryrun if (self.config['stop_email']['n_failed'] >= self.N_STOP_EMAIL_FAILED_LIMIT and now() - self.config['stop_email']['failed_time'] < timedelta(hours=2)): stop_email = True clear_email_task = False delete_info = Task.objects.filter( Q(is_sent=True, modified__lte=now() - timedelta(hours=1)) | Q(created__lte=now() - timedelta(days=1))).delete() logger.info(f'Tasks cleared: {delete_info}') qs = Task.unsent.all() qs = qs.select_related('notification__coder') qs = qs.prefetch_related( Prefetch( 'notification__coder__chat_set', queryset=Chat.objects.filter(is_group=False), to_attr='cchat', )) if stop_email: qs = qs.exclude(notification__method='email') if coders: qs = qs.filter(notification__coder__username__in=coders) if dryrun: qs = qs.order_by('modified') else: qs = qs.annotate(weight=Case( When(notification__method='email', then=1), default=0, output_field=PositiveSmallIntegerField(), )) qs = qs.order_by('weight', 'modified') done = 0 failed = 0 deleted = 0 for task in tqdm.tqdm(qs.iterator(), 'sending'): if stop_email and task.notification.method == settings.NOTIFICATION_CONF.EMAIL: if clear_email_task: contests = task.addition.get('contests', []) if contests and not Contest.objects.filter( pk__in=contests, start_time__gt=now()).exists(): task.delete() deleted += 1 continue try: notification = task.notification coder = notification.coder method = notification.method status = self.send_message( coder, method, task.addition, subject=task.subject, message=task.message, notification=notification, ) if status == 'removed': continue task.is_sent = True task.save() except Exception as e: logger.error('Exception sendout task:\n%s' % format_exc()) task.is_sent = False task.save() if isinstance(e, (SMTPResponseException, SMTPDataError)): stop_email = True if self.n_messages_sent: self.config['stop_email']['n_failed'] = 1 else: self.config['stop_email']['n_failed'] += 1 if self.config['stop_email'][ 'n_failed'] >= self.N_STOP_EMAIL_FAILED_LIMIT: clear_email_task = True self.config['stop_email']['failed_time'] = now() if task.is_sent: done += 1 else: failed += 1 logger.info(f'Done: {done}, failed: {failed}, deleted: {deleted}') self.save_config()
def __init__(self): self._logger = logging.getLogger('notify.error') coloredlogs.install(logger=self._logger) self._bot = Bot()
class Command(BaseCommand): help = 'Send out all unsent tasks' TELEGRAM_BOT = Bot() @print_sql_decorator() @transaction.atomic def handle(self, *args, **options): logger = getLogger('notification.sendout.tasks') qs = Task.unsent.all() qs = qs.select_related('notification__coder') qs = qs.prefetch_related( Prefetch( 'notification__coder__chat_set', queryset=Chat.objects.exclude(secret_key__isnull=True), to_attr='cchat', )) for task in qs: try: task.is_sent = True notification = task.notification coder = notification.coder if notification.method == Notification.TELEGRAM: will_mail = False if coder.chat and coder.chat.chat_id: try: if not coder.settings.get('telegram', {}).get( 'unauthorized', False): self.TELEGRAM_BOT.send_message( task.message, coder.chat.chat_id) except Unauthorized: coder.settings.setdefault( 'telegram', {})['unauthorized'] = True coder.save() will_mail = True else: will_mail = True if will_mail: # FIXME: skipping, fixed on https://yandex.ru/support/mail-new/web/spam/honest-mailers.html continue send_mail( settings.EMAIL_PREFIX_SUBJECT_ + task.subject, '%s, connect telegram chat by link %s.' % (coder.user.username, settings.HTTPS_HOST_ + reverse('telegram:me')), 'Contest list <*****@*****.**>', [coder.user.email], fail_silently=False, ) elif notification.method == Notification.EMAIL: # FIXME: skipping, fixed on https://yandex.ru/support/mail-new/web/spam/honest-mailers.html continue send_mail( settings.EMAIL_PREFIX_SUBJECT_ + task.subject, task.addition['text'], 'Contest list <*****@*****.**>', [coder.user.email], fail_silently=False, html_message=task.message, ) except Exception: logger.error('Exception sendout task:\n%s' % format_exc()) task.is_sent = False task.save()
class Command(BaseCommand): help = 'Send out all unsent tasks' TELEGRAM_BOT = Bot() @print_sql_decorator() @transaction.atomic def handle(self, *args, **options): logger = getLogger('notification.sendout.tasks') delete_info = Task.objects.filter(is_sent=True, modified__lte=now() - timedelta(days=31)).delete() logger.info(f'Tasks cleared: {delete_info}') qs = Task.unsent.all() qs = qs.select_related('notification__coder') qs = qs.prefetch_related( Prefetch( 'notification__coder__chat_set', queryset=Chat.objects.filter(is_group=False), to_attr='cchat', )) done = 0 failed = 0 for task in tqdm.tqdm(qs.iterator(), 'sending'): try: task.is_sent = True notification = task.notification coder = notification.coder method, *args = notification.method.split(':', 1) if method == Notification.TELEGRAM: will_mail = False if args: self.TELEGRAM_BOT.send_message(task.message, args[0]) elif coder.chat and coder.chat.chat_id: try: if not coder.settings.get('telegram', {}).get( 'unauthorized', False): self.TELEGRAM_BOT.send_message( task.message, coder.chat.chat_id) except Unauthorized: coder.settings.setdefault( 'telegram', {})['unauthorized'] = True coder.save() will_mail = True else: will_mail = True if will_mail: pass # FIXME: skipping, fixed on https://yandex.ru/support/mail-new/web/spam/honest-mailers.html # send_mail( # settings.EMAIL_PREFIX_SUBJECT_ + task.subject, # '%s, connect telegram chat by link %s.' % ( # coder.user.username, # settings.HTTPS_HOST_ + reverse('telegram:me') # ), # 'Contest list <*****@*****.**>', # [coder.user.email], # fail_silently=False, # ) elif method == Notification.EMAIL: pass # FIXME: skipping, fixed on https://yandex.ru/support/mail-new/web/spam/honest-mailers.html # send_mail( # settings.EMAIL_PREFIX_SUBJECT_ + task.subject, # task.addition['text'], # 'Contest list <*****@*****.**>', # [coder.user.email], # fail_silently=False, # html_message=task.message, # ) except Exception: logger.error('Exception sendout task:\n%s' % format_exc()) task.is_sent = False if task.is_sent: done += 1 else: failed += 1 task.save() logger.info(f'Done: {done}, failed: {failed}')
def __init__(self): self._bot = Bot()
def handle(self, *args, **options): self.stdout.write(str(options)) args = AttrDict(options) bot = Bot() if args.dump is not None and os.path.exists(args.dump): with open(args.dump, 'r') as fo: standings = json.load(fo) else: standings = {} problems_info = standings.setdefault('__problems_info', {}) parser_command = ParserCommand() iteration = 1 if args.dump else 0 while True: subprocess.call('clear', shell=True) print(now()) contest = Contest.objects.filter(pk=args.cid) parser_command.parse_statistic(contest, without_contest_filter=True) contest = contest.first() resource = contest.resource statistics = list(Statistics.objects.filter(contest=contest)) for p in problems_info.values(): if p.get('accepted') or not p.get('n_hidden'): p.pop('show_hidden', None) p['n_hidden'] = 0 updated = False has_hidden = False numbered = 0 for stat in sorted(statistics, key=lambda s: s.place_as_int): name_instead_key = resource.info.get( 'standings', {}).get('name_instead_key') name_instead_key = stat.account.info.get( '_name_instead_key', name_instead_key) if name_instead_key: name = stat.account.name else: name = stat.addition.get('name') if not name or not has_season(stat.account.key, name): name = stat.account.key filtered = False if args.query is not None and re.search( args.query, name, re.I): filtered = True message_id = None key = str(stat.account.id) if key in standings: if filtered: print(stat.modified, { k: v for k, v in standings[key].items() if k != 'problems' }, end=' | ') print(stat.place, stat.solving, end=' | ') problems = standings[key]['problems'] message_id = standings[key].get('messageId') def delete_message(): nonlocal message_id if message_id: for iteration in range(1, 5): try: bot.delete_message(chat_id=args.tid, message_id=message_id) message_id = None break except telegram.error.TimedOut as e: logger.warning(str(e)) time.sleep(iteration) continue p = [] has_update = False has_first_ac = False has_try_first_ac = False has_new_accepted = False for k, v in stat.addition.get('problems', {}).items(): p_info = problems_info.setdefault(k, {}) p_result = problems.get(k, {}).get('result') result = v['result'] is_hidden = str(result).startswith('?') is_accepted = str(result).startswith('+') try: is_accepted = is_accepted or float( result) > 0 and not v.get('partial') except Exception: pass if is_hidden: p_info['n_hidden'] = p_info.get('n_hidden', 0) + 1 if p_result != result or is_hidden: has_new_accepted |= is_accepted m = '%s%s %s' % (k, ('. ' + v['name']) if 'name' in v else '', result) if v.get('verdict'): m += ' ' + v['verdict'] if p_result != result: m = '*%s*' % m has_update = True if iteration: if p_info.get('show_hidden') == key: delete_message() if not is_hidden: p_info.pop('show_hidden') if not p_info.get('accepted'): if is_accepted: m += ' FIRST ACCEPTED' has_first_ac = True elif is_hidden and not p_info.get( 'show_hidden'): p_info['show_hidden'] = key m += ' TRY FIRST AC' has_try_first_ac = True if args.top and stat.place_as_int <= args.top: if not filtered: m += f' TOP{args.top}' filtered = True p.append(m) if result.startswith('+'): p_info['accepted'] = True has_hidden = has_hidden or is_hidden prev_place = standings[key].get('place') place = stat.place if has_new_accepted and prev_place: place = '%s->%s' % (prev_place, place) if args.numbered is not None and re.search( args.numbered, stat.account.key, re.I): numbered += 1 place = '%s (%s)' % (place, numbered) msg = '%s. _%s_' % (place, telegram.utils.helpers.escape_markdown( name.replace('_', ' '))) if p: msg = '%s, %s' % (', '.join(p), msg) if abs(standings[key]['solving'] - stat.solving) > 1e-9: msg += ' = %d' % stat.solving if 'penalty' in stat.addition: msg += f' ({stat.addition["penalty"]})' if has_update or has_first_ac or has_try_first_ac: updated = True if filtered: print(stat.place, stat.solving, end=' | ') if filtered: print(msg) if filtered and has_update or has_first_ac or has_try_first_ac: if not args.dryrun: delete_message() for iteration in range(1, 5): try: message = bot.send_message( msg=msg, chat_id=args.tid) message_id = message.message_id break except telegram.error.TimedOut as e: logger.warning(str(e)) time.sleep(iteration * 3) continue except telegram.error.BadRequest as e: logger.error(str(e)) break standings[key] = { 'solving': stat.solving, 'place': stat.place, 'problems': stat.addition.get('problems', {}), 'messageId': message_id, } if args.dump is not None and (updated or not os.path.exists(args.dump)): standings_dump = json.dumps(standings, indent=2) with open(args.dump, 'w') as fo: fo.write(standings_dump) if iteration: is_over = contest.end_time < now() if is_over and not has_hidden: break tick = 60 if is_over else 1 limit = now() + timedelta(seconds=args.delay * tick) size = 1 while now() < limit: value = humanize.naturaldelta(limit - now()) out = f'{value:{size}s}' size = len(value) print(out, end='\r') time.sleep(tick) print() iteration += 1
class Command(BaseCommand): help = 'Send out all unsent tasks' TELEGRAM_BOT = Bot() def add_arguments(self, parser): parser.add_argument('--coders', nargs='+') parser.add_argument('--dryrun', action='store_true', default=False) def get_message(self, method, data, **kwargs): subject_ = kwargs.pop('subject', None) message_ = kwargs.pop('message', None) if 'contests' in data: contests = Contest.objects.filter(pk__in=data['contests']) context = deepcopy(data.get('context', {})) context.update({ 'contests': contests, 'domain': settings.HTTPS_HOST_, }) context.update(kwargs) subject = render_to_string('subject', context).strip() context['subject'] = subject method = method.split(':', 1)[0] message = render_to_string('message/%s' % method, context).strip() else: subject = '' message = '' context = {} if subject_: subject = subject_ + subject if message_: message = message_ + message return subject, message, context def send_message(self, coder, method, data, **kwargs): method, *args = method.split(':', 1) subject, message, context = self.get_message(method=method, data=data, coder=coder, **kwargs) if method == settings.NOTIFICATION_CONF.TELEGRAM: if args: self.TELEGRAM_BOT.send_message(message, args[0], reply_markup=False) elif coder.chat and coder.chat.chat_id: try: if not coder.settings.get('telegram', {}).get('unauthorized', False): self.TELEGRAM_BOT.send_message(message, coder.chat.chat_id, reply_markup=False) except Unauthorized as e: if 'bot was blocked by the user' in str(e): coder.chat.delete() else: coder.settings.setdefault('telegram', {})['unauthorized'] = True coder.save() elif 'notification' in kwargs: kwargs['notification'].delete() elif method == settings.NOTIFICATION_CONF.EMAIL: send_mail( subject, message, 'CLIST <*****@*****.**>', [coder.user.email], fail_silently=False, html_message=message, ) elif method == settings.NOTIFICATION_CONF.WEBBROWSER: payload = { 'head': subject, 'body': message, } contests = list(context.get('contests', [])) if len(contests) == 1: contest = contests[0] payload['url'] = contest.url payload['icon'] = f'{settings.HTTPS_HOST_}/imagefit/static_resize/64x64/{contest.resource.icon}' send_user_notification( user=coder.user, payload=payload, ttl=300, ) @print_sql_decorator() @transaction.atomic def handle(self, *args, **options): coders = options.get('coders') dryrun = options.get('dryrun') logger = getLogger('notification.sendout.tasks') delete_info = Task.objects.filter( Q(is_sent=True, modified__lte=now() - timedelta(days=1)) | Q(modified__lte=now() - timedelta(days=7)) ).delete() logger.info(f'Tasks cleared: {delete_info}') if dryrun: qs = Task.objects.all() else: qs = Task.unsent.all() qs = qs.select_related('notification__coder') qs = qs.prefetch_related( Prefetch( 'notification__coder__chat_set', queryset=Chat.objects.filter(is_group=False), to_attr='cchat', ) ) if settings.STOP_EMAIL_: qs = qs.exclude(notification__method='email') if coders: qs = qs.filter(notification__coder__username__in=coders) if dryrun: qs = qs.order_by('-modified')[:1] done = 0 failed = 0 stop_email = settings.STOP_EMAIL_ for task in tqdm.tqdm(qs.iterator(), 'sending'): if stop_email and task.notification.method == settings.NOTIFICATION_CONF.EMAIL: continue try: task.is_sent = True task.save() notification = task.notification coder = notification.coder method = notification.method self.send_message( coder, method, task.addition, subject=task.subject, message=task.message, notification=notification, ) except Exception as e: logger.error('Exception sendout task:\n%s' % format_exc()) task.is_sent = False task.save() if isinstance(e, SMTPResponseException): stop_email = True if task.is_sent: done += 1 else: failed += 1 logger.info(f'Done: {done}, failed: {failed}')
def handle(self, *args, **options): self.stdout.write(str(options)) args = AttrDict(options) bot = Bot() if args.dump is not None and os.path.exists(args.dump): with open(args.dump, 'r') as fo: standings = json.load(fo) else: standings = {} problems_info = standings.setdefault('__problems_info', {}) parser_command = ParserCommand() iteration = 1 if args.dump else 0 while True: subprocess.call('clear', shell=True) contest = Contest.objects.get(pk=args.cid) parser_command.parse_statistic([contest], with_check=False) statistics = Statistics.objects.filter(contest=contest) updated = False has_hidden = False numbered = 0 for stat in sorted(statistics, key=lambda s: s.place_as_int): filtered = False if args.query is not None and re.search( args.query, stat.account.key, re.I): filtered = True message_id = None key = str(stat.account.id) if key in standings: problems = standings[key]['problems'] message_id = standings[key].get('messageId') p = [] has_update = False has_first_ac = False for k, v in stat.addition.get('problems', {}).items(): p_info = problems_info.setdefault(k, {}) p_result = problems.get(k, {}).get('result') result = v['result'] is_hidden = result.startswith('?') try: is_accepted = result.startswith('+') is_accepted = is_accepted or float(result) > 0 except Exception: pass if p_result != result or is_hidden: m = '%s%s %s' % (k, ('. ' + v['name']) if 'name' in v else '', result) if p_result != result: m = '*%s*' % m has_update = True if iteration and is_accepted and not p_info.get( 'accepted'): m += ' FIRST ACCEPTED' has_first_ac = True if is_accepted and args.top and stat.place_as_int <= args.top: filtered = True p.append(m) if result.startswith('+'): p_info['accepted'] = True has_hidden = has_hidden or is_hidden if args.numbered is not None and re.search( args.numbered, stat.account.key, re.I): numbered += 1 place = '%s (%s)' % (stat.place, numbered) else: place = stat.place msg = '%s. _%s_ %s' % (place, stat.account.key, ', '.join(p)) if standings[key]['solving'] != stat.solving: msg += ' = %d' % stat.solving if has_update or has_first_ac: updated = True if filtered: print(msg) if filtered and has_update or has_first_ac: if not args.dryrun: for _ in range(1, 5): try: if message_id: bot.delete_message( chat_id=args.tid, message_id=message_id) message = bot.send_message( msg=msg, chat_id=args.tid) message_id = message.message_id except telegram.error.TimedOut as e: logger.warning(str(e)) time.sleep(_ * 3) continue except telegram.error.BadRequest as e: logger.error(str(e)) break standings[key] = { 'solving': stat.solving, 'problems': stat.addition.get('problems', {}), 'messageId': message_id, } if args.dump is not None and (updated or not os.path.exists(args.dump)): standings_dump = json.dumps(standings, indent=2) with open(args.dump, 'w') as fo: fo.write(standings_dump) if iteration: is_over = contest.end_time < now() if is_over and not has_hidden: break time.sleep(args.delay * (60 if is_over else 1)) iteration += 1