diff --git a/p2d/domjudge_api.py b/p2d/domjudge_api.py index 9a0160c..82cc43e 100644 --- a/p2d/domjudge_api.py +++ b/p2d/domjudge_api.py @@ -1,5 +1,4 @@ import io -import logging import os import random import requests @@ -9,6 +8,7 @@ import yaml from p2d._version import __version__ +from p2d.logging_utils import logger def generate_externalid(problem): random_suffix = ''.join(random.choice(string.ascii_uppercase) for _ in range(6)) @@ -37,11 +37,11 @@ def update_problem_api(package_zip, problem_domjudge_id, credentials): credentials) if res.status_code != 200 or not res.json()['problem_id']: - logging.error('Error sending the package to the DOMjudge server: %s.' + logger.error('Error sending the package to the DOMjudge server: %s.' % res.json()) return False else: - logging.debug('Successfully sent the package to the DOMjudge server.') + logger.debug('Successfully sent the package to the DOMjudge server.') return True # Adds an "empty" problem to a contest. @@ -67,7 +67,7 @@ def add_problem_to_contest_api(problem, credentials): os.unlink(problem_yaml) if res.status_code != 200: - logging.error('Error adding the problem to the contest: %s.' % res.json()) + logger.error('Error adding the problem to the contest: %s.' % res.json()) return False problem['domjudge_id'] = res.json()[0] diff --git a/p2d/generate_domjudge_package.py b/p2d/generate_domjudge_package.py index 4d82ce5..72dad84 100644 --- a/p2d/generate_domjudge_package.py +++ b/p2d/generate_domjudge_package.py @@ -1,5 +1,4 @@ import json -import logging import os import pathlib import re @@ -12,6 +11,7 @@ import zipfile from p2d._version import __version__ +from p2d.logging_utils import logger from p2d import tex_utilities RESOURCES_PATH = os.path.join( @@ -55,12 +55,12 @@ # - problemname-solution.{tex,pdf} # params is a dictionary with keys contest_name, hide_balloon, hide_tlml. def generate_domjudge_package(problem, domjudge, tex_dir, params): - logging.debug('Creating the DOMjudge package directory \'%s\'.' % domjudge) + logger.debug('Creating the DOMjudge package directory \'%s\'.' % domjudge) problem_yaml_data = {} # Metadata - logging.debug('Writing \'domjudge-problem.ini\'.') + logger.debug('Writing \'domjudge-problem.ini\'.') ini_file = os.path.join(domjudge, 'domjudge-problem.ini') ini_content = [ 'short-name = %s' % problem['name'], @@ -81,7 +81,7 @@ def generate_domjudge_package(problem, domjudge, tex_dir, params): tex_utilities.generate_solution_pdf(problem, tex_dir, params) # Tests - logging.debug('Copying the tests in the DOMjudge package.') + logger.debug('Copying the tests in the DOMjudge package.') sample_dir = os.path.join(domjudge, 'data', 'sample') secret_dir = os.path.join(domjudge, 'data', 'secret') @@ -105,7 +105,7 @@ def generate_domjudge_package(problem, domjudge, tex_dir, params): os.path.join(domjudge, 'output_validators', 'interactor.cpp')) elif problem['checker']['name'] is not None: checker_name = problem['checker']['name'] - logging.debug('Standard checker \'%s\'.' % checker_name) + logger.debug('Standard checker \'%s\'.' % checker_name) checker_name_match = re.match(r'std\:\:([a-z0-9]+)\.cpp', checker_name) assert(checker_name_match) @@ -117,7 +117,7 @@ def generate_domjudge_package(problem, domjudge, tex_dir, params): problem_yaml_data['validator_flags'] = \ CHECKER_POLYGON2DOMJUDGE[checker_name] else: - logging.debug('Custom checker.') + logger.debug('Custom checker.') problem_yaml_data['validation'] = 'custom' pathlib.Path(domjudge, 'output_validators').mkdir() shutil.copyfile( @@ -141,7 +141,7 @@ def generate_domjudge_package(problem, domjudge, tex_dir, params): # Write problem.yaml yaml_path = os.path.join(domjudge, 'problem.yaml') - logging.debug( + logger.debug( 'Writing into \'%s\' the dictionary %s' % (yaml_path, problem_yaml_data)) with open(yaml_path, 'w', encoding='utf-8') as f: diff --git a/p2d/generate_testlib_for_domjudge.py b/p2d/generate_testlib_for_domjudge.py index fefe57a..2c2d791 100644 --- a/p2d/generate_testlib_for_domjudge.py +++ b/p2d/generate_testlib_for_domjudge.py @@ -1,9 +1,9 @@ -import logging import requests import os import re from p2d._version import __version__ +from p2d.logging_utils import logger HEADER_COMMENT = '''\ // Modified by a script to work with DOMjudge. @@ -121,12 +121,12 @@ def replace_function(lines, function): # The applied patch is copied from # https://github.com/cn-xcpc-tools/testlib-for-domjudge. def generate_testlib_for_domjudge(dst_path): - logging.debug('Downloading testlib.h from github.') + logger.debug('Downloading testlib.h from github.') req = requests.get( 'https://raw.githubusercontent.com/MikeMirzayanov/testlib/master/testlib.h') lines = req.text.splitlines() - logging.debug('Patching testlib.') + logger.debug('Patching testlib.') lines = add_header(lines, HEADER_COMMENT) for exit_code in NEW_EXIT_CODES: lines = replace_exit_code(lines, exit_code, NEW_EXIT_CODES[exit_code]) diff --git a/p2d/logging_utils.py b/p2d/logging_utils.py new file mode 100644 index 0000000..5de2938 --- /dev/null +++ b/p2d/logging_utils.py @@ -0,0 +1,50 @@ +import sys +import logging + +logger = logging.getLogger('Pol2Dom') + +# Custom formatter for the console logger handler. +# Adapted from https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output +class Pol2DomLoggingFormatter(logging.Formatter): + INDENT = 0 + + bold_gray = '\033[1;90m' + bold_green = '\033[1;92m' + bold_yellow = '\033[1;33m' + bold_red = '\033[1;91m' + yellow = '\033[33m' + red = '\033[91m' + reset = '\033[0m' + + COLORS = { + logging.DEBUG: (bold_gray, reset), + logging.INFO: (bold_green, reset), + logging.WARNING: (bold_yellow, yellow), + logging.ERROR: (bold_red, red) + } + + def format(self, record): + level_color, message_color = self.COLORS.get(record.levelno) + padding = ' ' * (10 - len(record.levelname)) + log_fmt = ' ' * self.INDENT + level_color + '{levelname}' + self.reset + padding + message_color + '{message}' + self.reset + formatter = logging.Formatter(log_fmt, style='{') + return formatter.format(record) + +def configure_logging(verbosity): + logging.basicConfig( + stream=sys.stdout, + format='{levelname}\t{message}', + style='{', + level=eval('logging.' + verbosity.upper()) + ) + + requests_log = logging.getLogger('requests.packages.urllib3') + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True + + logger.setLevel(eval('logging.' + verbosity.upper())) + console_handler = logging.StreamHandler() + console_handler.setLevel(eval('logging.' + verbosity.upper())) + console_handler.setFormatter(Pol2DomLoggingFormatter()) + logger.addHandler(console_handler) + logger.propagate = False diff --git a/p2d/p2d.py b/p2d/p2d.py index 145e380..68d4245 100644 --- a/p2d/p2d.py +++ b/p2d/p2d.py @@ -5,12 +5,14 @@ from argparse import ArgumentParser from p2d._version import __version__ +from p2d.logging_utils import logger from p2d import (domjudge_api, generate_domjudge_package, generate_testlib_for_domjudge, parse_polygon_package, polygon_api, p2d_utils, + logging_utils, tex_utilities) RESOURCES_PATH = os.path.join( os.path.split(os.path.realpath(__file__))[0], 'resources') @@ -19,7 +21,7 @@ def prepare_argument_parser(): parser = ArgumentParser(description='Utility script to import a whole contest from polygon into DOMjudge.') parser.add_argument('contest_directory', help='The directory containing the config.yaml file describing the contest. This directory will store also the polygon and DOMjudge packages.') - parser.add_argument('--problem', help='Use this flag to pass the name of a problem if you want to execute the script on a single problem instead of all the problems.') + parser.add_argument('--problems', nargs='+', help='Use this flag to pass the name of one or more problems if you want to execute the script on only on those problems.') parser.add_argument('--polygon', '--import', '--get', '--download', action='store_true', help='Whether the problem packages should be downloaded from Polygon. Otherwise only the packages already present in the system will be considered.') parser.add_argument('--convert', action='store_true', help='Whether the polygon packages should be converted to DOMjudge packages. Otherwise only the DOMjudge packages already present in the system will be considered.') parser.add_argument('--domjudge', '--export', '--send', '--upload', action='store_true', help='Whether the DOMjudge packages shall be uploaded to the DOMjudge instance specified in config.yaml.') @@ -36,13 +38,13 @@ def prepare_argument_parser(): def p2d(args): - p2d_utils.configure_logging(args.verbosity) + logging_utils.configure_logging(args.verbosity) # Downloading and patching testlib.h if necessary. testlib_h = os.path.join(RESOURCES_PATH, 'testlib.h') if not os.path.isfile(testlib_h) or args.update_testlib: generate_testlib_for_domjudge.generate_testlib_for_domjudge(testlib_h) - logging.info('The file testlib.h was successfully downloaded and patched. The local version can be found at \'%s\'.' % testlib_h) + logger.info('The file testlib.h was successfully downloaded and patched. The local version can be found at \'%s\'.' % testlib_h) contest_dir = args.contest_directory @@ -54,34 +56,34 @@ def p2d(args): and args.from_contest is None \ and not args.pdf_contest \ and not args.clear_dir and not args.clear_domjudge_ids: - logging.error('At least one of the flags --polygon, --convert, --domjudge, --from-contest, --contestpdf, --clear-dir, --clear-domjudge-ids is necessary.') + logger.error('At least one of the flags --polygon, --convert, --domjudge, --from-contest, --contestpdf, --clear-dir, --clear-domjudge-ids is necessary.') exit(1) if args.clear_dir: for problem in config['problems']: - if args.problem and args.problem != problem['name']: + if args.problems and problem['name'] not in args.problems: continue p2d_utils.remove_problem_data(problem, contest_dir) p2d_utils.save_config_yaml(config, contest_dir) - logging.info('Deleted the problems\' data from \'%s\'.' % contest_dir) + logger.info('Deleted the problems\' data from \'%s\'.' % contest_dir) if args.clear_domjudge_ids: for problem in config['problems']: - if args.problem and args.problem != problem['name']: + if args.problems and problem['name'] not in args.problems: continue problem['domjudge_server_version'] = -1 problem.pop('domjudge_id', None) problem.pop('domjudge_externalid', None) p2d_utils.save_config_yaml(config, contest_dir) - logging.info('Deleted the DOMjudge IDs from config.yaml.') + logger.info('Deleted the DOMjudge IDs from config.yaml.') if (args.polygon or args.from_contest) \ and ('polygon' not in config or 'key' not in config['polygon'] or 'secret' not in config['polygon']): - logging.error('The entries polygon:key and polygon:secret must be ' + logger.error('The entries polygon:key and polygon:secret must be ' 'present in config.yaml to access Polygon problems.') exit(1) @@ -90,7 +92,7 @@ def p2d(args): or 'server' not in config['domjudge'] or 'username' not in config['domjudge'] or 'password' not in config['domjudge']): - logging.error('The entries domjudge:contest_id, domjudge:server, ' + logger.error('The entries domjudge:contest_id, domjudge:server, ' 'domjudge:username, domjudge:password must be present ' 'in config.yaml to upload problems on DOMjudge.') exit(1) @@ -111,16 +113,17 @@ def p2d(args): # 3. Upload the DOMjudge package (to a running DOMjudge server). problem_selected_exists = False for problem in config['problems']: - if args.problem and args.problem != problem['name']: + if args.problems and problem['name'] not in args.problems: continue if not args.polygon and not args.convert and not args.domjudge: continue problem_selected_exists = True - print('\033[1m' + problem['name'] + '\033[0m') # Bold + print('Processing problem \033[96m' + problem['name'] + '\033[39m') # Cyan + logging_utils.Pol2DomLoggingFormatter.INDENT += 1 if 'label' not in problem: - logging.warning('The problem does not have a label.') + logger.warning('The problem does not have a label.') if args.polygon: if args.no_cache: @@ -148,14 +151,14 @@ def p2d(args): contest_dir, 'domjudge', problem['name']), problem) p2d_utils.save_config_yaml(config, contest_dir) - print() + logging_utils.Pol2DomLoggingFormatter.INDENT -= 1 - if args.problem and not problem_selected_exists: - logging.warning('The problem specified with --problem does not appear ' + if args.problems and not problem_selected_exists: + logger.warning('The problem specified with --problem does not appear ' 'in config.yaml.') return - if args.problem: + if args.problems: return if args.pdf_contest: @@ -163,16 +166,16 @@ def p2d(args): # Guidelines for error tracing and logging: # -# Use logging everywhere for info/warning/error printing. +# Use logging_utils.logger everywhere for info/warning/error printing. # Do not use print. # Use exceptions when appropriate. # -# For errors, use logging.error followed by exit(1) or raise an exception. -# For warnings, use logging.warning. +# For errors, use logger.error followed by exit(1) or raise an exception. +# For warnings, use logger.warning. # # For information: -# - Use logging.info in this p2d.py (for useful information). -# - Use logging.debug in all other files (and for not-so-useful information +# - Use logger.info in this p2d.py (for useful information). +# - Use logger.debug in all other files (and for not-so-useful information # in this file). def main(): args = prepare_argument_parser().parse_args() diff --git a/p2d/p2d_utils.py b/p2d/p2d_utils.py index 821c541..0ec9cd9 100644 --- a/p2d/p2d_utils.py +++ b/p2d/p2d_utils.py @@ -1,5 +1,4 @@ import json -import logging import os import pathlib import re @@ -12,6 +11,7 @@ import zipfile from p2d._version import __version__ +from p2d.logging_utils import logger from p2d import (domjudge_api, generate_domjudge_package, generate_testlib_for_domjudge, @@ -24,7 +24,7 @@ def manage_download(config, polygon_dir, problem): if 'polygon_id' not in problem: - logging.warning('Skipped because polygon_id is not specified.') + logger.warning('Skipped because polygon_id is not specified.') return # Check versions @@ -32,19 +32,19 @@ def manage_download(config, polygon_dir, problem): latest_package = polygon_api.get_latest_package_id( config['polygon']['key'], config['polygon']['secret'], problem['polygon_id']) - logging.debug('For problem %s the selected package is %s.' + logger.debug('For problem %s the selected package is %s.' % (problem['name'], latest_package[1])) if latest_package[0] == -1: - logging.warning('No packages were found on polygon.') + logger.warning('No packages were found on polygon.') return if latest_package[0] < local_version: - logging.warning('The local version is newer than the polygon version.') + logger.warning('The local version is newer than the polygon version.') return if latest_package[0] == local_version: - logging.info('The polygon package is up to date.') + logger.info('The polygon package is up to date.') return pathlib.Path(polygon_dir).mkdir(exist_ok=True) @@ -57,16 +57,16 @@ def manage_download(config, polygon_dir, problem): # Unzip the package if not zipfile.is_zipfile(package_zip): - logging.error( + logger.error( 'There was an error downloading the package zip to \'%s\'.' % package_zip) return with zipfile.ZipFile(package_zip, 'r') as f: - logging.debug('Unzipping the polygon package \'%s\'.' % package_zip) + logger.debug('Unzipping the polygon package \'%s\'.' % package_zip) f.extractall(polygon_dir) - logging.info('Downloaded and unzipped the polygon package into ' + logger.info('Downloaded and unzipped the polygon package into ' '\'%s\'.' % os.path.join(polygon_dir)) problem['polygon_version'] = latest_package[0] @@ -83,30 +83,30 @@ def manage_convert(config, polygon_dir, domjudge_dir, tex_dir, problem): domjudge_version = problem.get('domjudge_local_version', -1) if polygon_version == -1: - logging.warning('The polygon package is not present locally.') + logger.warning('The polygon package is not present locally.') return if polygon_version < domjudge_version: - logging.warning('The version of the local domjudge package is more ' + logger.warning('The version of the local domjudge package is more ' 'up to date the the local polygon package.') return if polygon_version == domjudge_version: - logging.info('The local domjudge package is already up to date.') + logger.info('The local domjudge package is already up to date.') return # Parse the polygon package problem_package = parse_polygon_package.parse_problem_from_polygon(polygon_dir) if problem_package['name'] != problem['name']: - logging.error('The name of the problem does not coincide with the name of the problem in polygon, which is \'%s\'.' % problem_package['name']) + logger.error('The name of the problem does not coincide with the name of the problem in polygon, which is \'%s\'.' % problem_package['name']) exit(1) # Set some additional properties of the problem (not present in polygon) missing_keys = list(filter(lambda key: key not in problem or not problem[key], ['label', 'color', 'author', 'preparation'])) if missing_keys: - logging.warning('The keys %s are not set in config.yaml for this problem.' % missing_keys) + logger.warning('The keys %s are not set in config.yaml for this problem.' % missing_keys) problem_package['label'] = problem.get('label', '?') problem_package['color'] = convert_to_hex(problem.get('color', 'Black')) @@ -120,7 +120,7 @@ def manage_convert(config, polygon_dir, domjudge_dir, tex_dir, problem): problem_package['author'] = problem.get('author', '') problem_package['preparation'] = problem.get('preparation', '') - logging.debug(json.dumps(problem_package, sort_keys=True, indent=4)) + logger.debug(json.dumps(problem_package, sort_keys=True, indent=4)) # Generate the tex sources of statement and solution. problem_tex = tex_utilities.generate_statement_tex(problem_package, tex_dir) @@ -150,7 +150,7 @@ def manage_convert(config, polygon_dir, domjudge_dir, tex_dir, problem): 'hide_tlml': config.get('hide_tlml', 0) }) - logging.info('Converted the polygon package to the DOMjudge package \'%s\'.', + logger.info('Converted the polygon package to the DOMjudge package \'%s\'.', domjudge_dir) # Zip the package @@ -167,23 +167,23 @@ def manage_domjudge(config, domjudge_dir, problem): server_version = problem.get('domjudge_server_version', -1) if local_version == -1: - logging.warning('The DOMjudge package is not present locally.') + logger.warning('The DOMjudge package is not present locally.') return if local_version < server_version: - logging.warning('The version of the DOMjudge package on the server is ' + logger.warning('The version of the DOMjudge package on the server is ' 'more up to date than the local one.') return if local_version == server_version: - logging.info('The DOMjudge package on the server is already up to date.') + logger.info('The DOMjudge package on the server is already up to date.') return # Adding the problem to the contest if it was not already done. if 'domjudge_id' not in problem: if not domjudge_api.add_problem_to_contest_api(problem, config['domjudge']): - logging.error('There was an error while adding the problem ' + logger.error('There was an error while adding the problem ' 'to the contest in the DOMjudge server.') return @@ -196,13 +196,13 @@ def manage_domjudge(config, domjudge_dir, problem): if not domjudge_api.update_problem_api( zip_file_copy, problem['domjudge_id'], config['domjudge']): - logging.error('There was an error while updating the problem ' + logger.error('There was an error while updating the problem ' 'in the DOMjudge server.') return problem['domjudge_server_version'] = local_version - logging.info('Updated the DOMjudge package on the server \'%s\', with id = \'%s\'.' % (config['domjudge']['server'], problem['domjudge_id'])) + logger.info('Updated the DOMjudge package on the server \'%s\', with id = \'%s\'.' % (config['domjudge']['server'], problem['domjudge_id'])) # Updates config with the data of the problems in the specified contest. def fill_config_from_contest(config, contest_id): @@ -210,7 +210,7 @@ def fill_config_from_contest(config, contest_id): config['polygon']['key'], config['polygon']['secret'], contest_id ) - logging.info('Fetched problems from contest {}.'.format(contest_id)) + logger.info('Fetched problems from contest {}.'.format(contest_id)) new_problems = [] @@ -229,9 +229,9 @@ def fill_config_from_contest(config, contest_id): config_problem['label'] = label if len(new_problems) > 0: - logging.info('Found new problems: {}.'.format(', '.join(new_problems))) + logger.info('Found new problems: {}.'.format(', '.join(new_problems))) else: - logging.info('No new problems were found in the contest.') + logger.info('No new problems were found in the contest.') # Generates contest_dir/tex/problemset.pdf and contest_dir/tex/solutions.pdf. def generate_problemset_solutions(config, contest_dir): @@ -258,31 +258,20 @@ def generate_problemset_solutions(config, contest_dir): os.path.join(contest_dir, 'tex'), pdf_generation_params) - logging.info('Successfully generated \'%s\' and \'%s\'.' % + logger.info('Successfully generated \'%s\' and \'%s\'.' % (os.path.join(contest_dir, 'tex', 'problemset.pdf'), os.path.join(contest_dir, 'tex', 'solutions.pdf'))) - -def configure_logging(verbosity): - logging.basicConfig( - stream=sys.stdout, - format='%(levelname)s: %(message)s', - level=eval('logging.' + verbosity.upper()) - ) - - requests_log = logging.getLogger("requests.packages.urllib3") - requests_log.setLevel(logging.DEBUG) - requests_log.propagate = True def load_config_yaml(contest_dir): config_yaml = os.path.join(contest_dir, 'config.yaml') if not os.path.isfile(config_yaml): - logging.error('The file %s was not found.' % config_yaml) + logger.error('The file %s was not found.' % config_yaml) exit(1) with open(config_yaml, 'r') as f: try: config = yaml.safe_load(f) - logging.debug(config) + logger.debug(config) return config except yaml.YAMLError as exc: # TODO: Is this except meaningful? print(exc) @@ -292,35 +281,35 @@ def load_config_yaml(contest_dir): # mandatory keys and checking that no unexpected keys are present. def validate_config_yaml(config): if 'contest_name' not in config or 'problems' not in config: - logging.error('The keys \'contest_name\' and \'problems\' must be present in \'config.yaml\'.') + logger.error('The keys \'contest_name\' and \'problems\' must be present in \'config.yaml\'.') exit(1) top_level_keys = ['contest_name', 'polygon', 'domjudge', 'front_page_problemset', 'front_page_solutions', 'problems'] wrong_keys = list(set(config.keys()) - set(top_level_keys)) if wrong_keys: - logging.warning( + logger.warning( 'The key \'%s\' is not expected as top-level key in \'config.yaml\'. The expected keys are: %s.' % (wrong_keys[0], ', '.join(top_level_keys))) polygon_keys = ['key', 'secret'] domjudge_keys = ['server', 'username', 'password', 'contest_id'] if 'polygon' in config and\ set(config['polygon'].keys()) != set(polygon_keys): - logging.warning('The subdictionary \'polygon\' of \'config.yaml\' must contain they keys: %s.' % ', '.join(polygon_keys)) + logger.warning('The subdictionary \'polygon\' of \'config.yaml\' must contain they keys: %s.' % ', '.join(polygon_keys)) if 'domjudge' in config and\ set(config['domjudge'].keys()) != set(domjudge_keys): - logging.warning('The subdictionary \'domjudge\' of \'config.yaml\' must contain they keys: %s.' % ', '.join(domjudge_keys)) + logger.warning('The subdictionary \'domjudge\' of \'config.yaml\' must contain they keys: %s.' % ', '.join(domjudge_keys)) problem_keys = ['name', 'label', 'color', 'author', 'preparation', 'override_time_limit', 'override_memory_limit', 'polygon_id', 'polygon_version', 'domjudge_local_version', 'domjudge_server_version', 'domjudge_id', 'domjudge_externalid'] for problem in config['problems']: if 'name' not in problem: - logging.error('All problems described in \'config.yaml\' must contain the key \'name\'.') + logger.error('All problems described in \'config.yaml\' must contain the key \'name\'.') exit(1) wrong_keys = list(set(problem.keys()) - set(problem_keys)) if wrong_keys: - logging.warning('The key \'%s\' in the description of problem \'%s\' in \'config.yaml\' is not expected. The expected keys are: %s.' % (wrong_keys[0], problem['name'], ', '.join(problem_keys))) + logger.warning('The key \'%s\' in the description of problem \'%s\' in \'config.yaml\' is not expected. The expected keys are: %s.' % (wrong_keys[0], problem['name'], ', '.join(problem_keys))) def save_config_yaml(config, contest_dir): with open(os.path.join(contest_dir, 'config.yaml'), 'w', encoding='utf-8') as f: @@ -375,13 +364,13 @@ def convert_to_hex(color): if color[0] == '#': color = color[1:] if not re.fullmatch(r'[A-Fa-f0-9]{6}', color): - logging.error(error_message) + logger.error(error_message) exit(1) else: try: color = webcolors.name_to_hex(color)[1:] except ValueError: - logging.error(error_message) + logger.error(error_message) exit(1) return color.upper() diff --git a/p2d/parse_polygon_package.py b/p2d/parse_polygon_package.py index 7280cb0..4bae279 100644 --- a/p2d/parse_polygon_package.py +++ b/p2d/parse_polygon_package.py @@ -1,5 +1,4 @@ import json -import logging import os import pathlib import re @@ -8,6 +7,7 @@ import xml.etree.ElementTree from p2d._version import __version__ +from p2d.logging_utils import logger def parse_samples_explanations(notes): lines = notes.splitlines() @@ -17,13 +17,13 @@ def parse_samples_explanations(notes): for line in lines: if re.fullmatch(r'%BEGIN (\d+)', line.strip()): if test_id != -1: - logging.error('In the samples explanations, there are two %BEGIN lines without an %END line in between: %s.' % notes) + logger.error('In the samples explanations, there are two %BEGIN lines without an %END line in between: %s.' % notes) exit(1) assert(test_id == -1) test_id = int(re.fullmatch(r'%BEGIN (\d+)', line.strip()).group(1)) elif re.fullmatch(r'%END', line.strip()): if test_id == -1: - logging.error('In the samples explanations, there is an %END line which does not close any %BEGIN line: %s.' % notes) + logger.error('In the samples explanations, there is an %END line which does not close any %BEGIN line: %s.' % notes) exit(1) assert(test_id != -1) assert(test_id not in explanations) @@ -34,7 +34,7 @@ def parse_samples_explanations(notes): elif test_id != -1: curr += line + '\n' if test_id != -1: - logging.error('In the samples explanations, the last %BEGIN line is not matched by an %END line: %s.' % notes) + logger.error('In the samples explanations, the last %BEGIN line is not matched by an %END line: %s.' % notes) exit(1) assert(test_id == -1) return explanations @@ -85,15 +85,15 @@ def parse_problem_from_polygon(polygon): def pol_path(*path): return os.path.join(polygon, *path) - logging.debug('Parsing the polygon package directory \'%s\'.' % polygon) + logger.debug('Parsing the polygon package directory \'%s\'.' % polygon) if not os.path.isfile(pol_path('problem.xml')): - logging.error('The directory \'%s\' is not a polygon package (as it does not contain the file \'problem.xml\'.' % polygon) + logger.error('The directory \'%s\' is not a polygon package (as it does not contain the file \'problem.xml\'.' % polygon) exit(1) problem = {} # Metadata - logging.debug('Parsing \'%s\'' % pol_path('problem.xml')) + logger.debug('Parsing \'%s\'' % pol_path('problem.xml')) problem_xml = xml.etree.ElementTree.parse(pol_path('problem.xml')) problem['name'] = problem_xml.getroot().attrib['short-name'] problem['title'] = problem_xml.find('names').find('name').attrib['value'] @@ -147,7 +147,7 @@ def pol_path(*path): test_id = 1 for testset in problem_xml.find('judging').iter('testset'): if testset.attrib['name'] not in ['pretests', 'tests']: - logging.warning('testset \'%s\' ignored: only the testset \'tests\' is exported in DOMjudge (apart from the samples).' % testset.attrib['name']) + logger.warning('testset \'%s\' ignored: only the testset \'tests\' is exported in DOMjudge (apart from the samples).' % testset.attrib['name']) local_id = 1 # Pretests are processed only to collect samples. @@ -188,7 +188,7 @@ def pol_path(*path): # Interactor problem['interactor'] = None if problem_xml.find('assets').find('interactor'): - logging.debug('The problem is interactive.') + logger.debug('The problem is interactive.') problem['interactor'] = { 'source': pol_path(problem_xml.find('assets').find('interactor') .find('source').attrib['path']) diff --git a/p2d/polygon_api.py b/p2d/polygon_api.py index f4fa5ba..0d173ab 100644 --- a/p2d/polygon_api.py +++ b/p2d/polygon_api.py @@ -1,6 +1,5 @@ import hashlib import io -import logging import os import random import requests @@ -9,6 +8,7 @@ import time from p2d._version import __version__ +from p2d.logging_utils import logger POLYGON_ADDRESS = 'https://polygon.codeforces.com/api/' @@ -34,13 +34,13 @@ def call_polygon_api(key, secret, method_name, params): to_hash = pref + middle + suff params['apiSig'] = rand + hashlib.sha512(to_hash.encode()).hexdigest() - logging.debug('Sending API request:\n' + logger.debug('Sending API request:\n' + ('\t method = %s\n' % method_name) + '\t params = %s' % params) res = requests.post(POLYGON_ADDRESS + method_name, data = params) if not res.ok: - logging.error('API call to polygon returned status %s. The content of the response is %s.' % (res.status_code, res.text)) + logger.error('API call to polygon returned status %s. The content of the response is %s.' % (res.status_code, res.text)) exit(1) assert(res.ok) return res @@ -53,7 +53,7 @@ def get_latest_package_id(key, secret, problem_id): {'problemId': problem_id}).json() if packages_list['status'] != 'OK': - logging.error('API problem.packages request to polygon failed with error: %s' % packages_list['comment']) + logger.error('API problem.packages request to polygon failed with error: %s' % packages_list['comment']) exit(1) revision = -1 diff --git a/p2d/tex_utilities.py b/p2d/tex_utilities.py index 2495937..8f020ed 100644 --- a/p2d/tex_utilities.py +++ b/p2d/tex_utilities.py @@ -1,4 +1,3 @@ -import logging import os import pathlib import re @@ -8,6 +7,7 @@ import tempfile from p2d._version import __version__ +from p2d.logging_utils import logger RESOURCES_PATH = os.path.join( os.path.split(os.path.realpath(__file__))[0], 'resources') @@ -15,9 +15,9 @@ # Execute pdflatex on tex_file. # tex_file is a .tex file def tex2pdf(tex_file): - logging.debug('Executing pdflatex on \'%s\'.' % tex_file) + logger.debug('Executing pdflatex on \'%s\'.' % tex_file) if not tex_file.endswith('.tex'): - logging.error('The argument tex_file=\'%s\' passed to tex2pdf is not a .tex file.' % tex_file) + logger.error('The argument tex_file=\'%s\' passed to tex2pdf is not a .tex file.' % tex_file) exit(1) tex_dir = os.path.dirname(tex_file) @@ -25,13 +25,13 @@ def tex2pdf(tex_file): command_as_list = ['pdflatex', '-interaction=nonstopmode', '--shell-escape', '-output-dir=' + tex_dir, '-jobname=%s' % tex_name, tex_file] - logging.debug('pdflatex command = ' + ' '.join(command_as_list)) + logger.debug('pdflatex command = ' + ' '.join(command_as_list)) pdflatex = subprocess.run(command_as_list, stdout=subprocess.PIPE, shell=False) if pdflatex.returncode != 0: - logging.error(' '.join(command_as_list) + '\n' + logger.error(' '.join(command_as_list) + '\n' + pdflatex.stdout.decode("utf-8")) - logging.error('The pdflatex command returned an error.') + logger.error('The pdflatex command returned an error.') exit(1) tex_pdf = os.path.join(tex_dir, tex_name + '.pdf') @@ -62,7 +62,7 @@ def generate_statement_tex(problem, tex_dir): samples_tex += '\\sampleexplanation{%s}\n' % sample['explanation'] if sample_cnt == 0: - logging.error('No samples found.') + logger.error('No samples found.') exit(1) with open(os.path.join(RESOURCES_PATH, 'statement_template.tex')) as f: @@ -200,7 +200,7 @@ def generate_problemset_pdf(problems, frontpage, tex_dir, params): for problem in problems: maybe_tex = os.path.join(tex_dir, problem + '-statement-content.tex') if not os.path.isfile(maybe_tex): - logging.warning('The tex source \'%s\' does not exist; but it is required to generate the pdf with all problems.' % maybe_tex) + logger.warning('The tex source \'%s\' does not exist; but it is required to generate the pdf with all problems.' % maybe_tex) continue problemset_tex += '\\input{%s-statement-content.tex}\n' % problem problemset_tex += '\\insertblankpageifnecessary\n\n' @@ -228,7 +228,7 @@ def generate_solutions_pdf(problems, frontpage, tex_dir, params): for problem in problems: maybe_tex = os.path.join(tex_dir, problem + '-solution-content.tex') if not os.path.isfile(maybe_tex): - logging.warning('The tex source \'%s\' does not exist; but it is required to generate the pdf with all solutions.' % maybe_tex) + logger.warning('The tex source \'%s\' does not exist; but it is required to generate the pdf with all solutions.' % maybe_tex) continue solutions_tex += '\\input{%s-solution-content.tex}\n' % problem solutions_tex += '\\clearpage\n'