diff --git a/readthedocs/doc_builder/environments.py b/readthedocs/doc_builder/environments.py index eb5002a5bd5..0a2d570ba8b 100644 --- a/readthedocs/doc_builder/environments.py +++ b/readthedocs/doc_builder/environments.py @@ -2,22 +2,26 @@ """Documentation Builder Environments.""" -from __future__ import absolute_import -from builtins import str -from builtins import object +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + +import logging import os import re -import sys -import logging +import socket import subprocess +import sys import traceback -import socket from datetime import datetime +import six +from builtins import object, str from django.conf import settings from django.utils.translation import ugettext_lazy as _ from docker import APIClient -from docker.errors import APIError as DockerAPIError, DockerException +from docker.errors import APIError as DockerAPIError +from docker.errors import DockerException +from requests.exceptions import ConnectionError from slumber.exceptions import HttpClientError from readthedocs.builds.constants import BUILD_STATE_FINISHED @@ -25,24 +29,25 @@ from readthedocs.core.utils import slugify from readthedocs.projects.constants import LOG_TEMPLATE from readthedocs.restapi.client import api as api_v2 -from requests.exceptions import ConnectionError -from .exceptions import (BuildEnvironmentException, BuildEnvironmentError, - BuildEnvironmentWarning, BuildEnvironmentCreationFailed) -from .constants import (DOCKER_SOCKET, DOCKER_VERSION, DOCKER_IMAGE, - DOCKER_LIMITS, DOCKER_TIMEOUT_EXIT_CODE, - DOCKER_OOM_EXIT_CODE, SPHINX_TEMPLATE_DIR, - MKDOCS_TEMPLATE_DIR, DOCKER_HOSTNAME_MAX_LEN) -import six +from .constants import ( + DOCKER_HOSTNAME_MAX_LEN, DOCKER_IMAGE, DOCKER_LIMITS, DOCKER_OOM_EXIT_CODE, + DOCKER_SOCKET, DOCKER_TIMEOUT_EXIT_CODE, DOCKER_VERSION, + MKDOCS_TEMPLATE_DIR, SPHINX_TEMPLATE_DIR) +from .exceptions import ( + BuildEnvironmentCreationFailed, BuildEnvironmentError, + BuildEnvironmentException, BuildEnvironmentWarning, BuildTimeoutError, + ProjectBuildsSkippedError, VersionLockedError, YAMLParseError) log = logging.getLogger(__name__) - __all__ = ( 'api_v2', - 'BuildCommand', 'DockerBuildCommand', + 'BuildCommand', + 'DockerBuildCommand', 'LocalEnvironment', - 'LocalBuildEnvironment', 'DockerBuildEnvironment', + 'LocalBuildEnvironment', + 'DockerBuildEnvironment', ) @@ -218,8 +223,12 @@ def run(self): :type cmd_input: str :param combine_output: combine STDERR into STDOUT """ - log.info("Running in container %s: '%s' [%s]", - self.build_env.container_id, self.get_command(), self.cwd) + log.info( + "Running in container %s: '%s' [%s]", + self.build_env.container_id, + self.get_command(), + self.cwd, + ) self.start_time = datetime.utcnow() client = self.build_env.get_client() @@ -228,7 +237,7 @@ def run(self): container=self.build_env.container_id, cmd=self.get_wrapped_command(), stdout=True, - stderr=True + stderr=True, ) output = client.exec_start(exec_id=exec_cmd['Id'], stream=False) @@ -409,6 +418,16 @@ class BuildEnvironment(BaseEnvironment): successful """ + # Exceptions considered ERROR from a Build perspective but as a WARNING for + # the application itself. These exception are logged as warning and not sent + # to Sentry. + WARNING_EXCEPTIONS = ( + VersionLockedError, + ProjectBuildsSkippedError, + YAMLParseError, + BuildTimeoutError, + ) + def __init__(self, project=None, version=None, build=None, config=None, record=True, environment=None, update_on_success=True): super(BuildEnvironment, self).__init__(project, environment) @@ -427,39 +446,57 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, tb): ret = self.handle_exception(exc_type, exc_value, tb) self.update_build(BUILD_STATE_FINISHED) - log.info(LOG_TEMPLATE - .format(project=self.project.slug, - version=self.version.slug, - msg='Build finished')) + log.info( + LOG_TEMPLATE.format( + project=self.project.slug, + version=self.version.slug, + msg='Build finished', + ) + ) return ret def handle_exception(self, exc_type, exc_value, _): """ - Exception handling for __enter__ and __exit__ + Exception handling for __enter__ and __exit__. This reports on the exception we're handling and special cases - subclasses of BuildEnvironmentException. For + subclasses of BuildEnvironmentException. For :py:class:`BuildEnvironmentWarning`, exit this context gracefully, but - don't mark the build as a failure. For all other exception classes, + don't mark the build as a failure. For all other exception classes, including :py:class:`BuildEnvironmentError`, the build will be marked as a failure and the context will be gracefully exited. + + If the exception's type is :py:class:`BuildEnvironmentWarning` or it's + an exception marked as ``WARNING_EXCEPTIONS`` we log the problem as a + WARNING, otherwise we log it as an ERROR. """ if exc_type is not None: - if not issubclass(exc_type, BuildEnvironmentWarning): - log.error(LOG_TEMPLATE - .format(project=self.project.slug, - version=self.version.slug, - msg=exc_value), - exc_info=True, - extra={ - 'stack': True, - 'tags': { - 'build': self.build.get('id'), - 'project': self.project.slug, - 'version': self.version.slug, - }, - }) + log_level_function = None + if issubclass(exc_type, BuildEnvironmentWarning): + log_level_function = log.warning + elif exc_type in self.WARNING_EXCEPTIONS: + log_level_function = log.warning self.failure = exc_value + else: + log_level_function = log.error + self.failure = exc_value + + log_level_function( + LOG_TEMPLATE.format( + project=self.project.slug, + version=self.version.slug, + msg=exc_value, + ), + exc_info=True, + extra={ + 'stack': True, + 'tags': { + 'build': self.build.get('id'), + 'project': self.project.slug, + 'version': self.version.slug, + }, + }, + ) return True def record_command(self, command): @@ -467,11 +504,13 @@ def record_command(self, command): def _log_warning(self, msg): # :'( - log.warning(LOG_TEMPLATE.format( - project=self.project.slug, - version=self.version.slug, - msg=msg, - )) + log.warning( + LOG_TEMPLATE.format( + project=self.project.slug, + version=self.version.slug, + msg=msg, + ) + ) def run(self, *cmd, **kwargs): kwargs.update({ @@ -529,15 +568,18 @@ def update_build(self, state=None): # TODO drop exit_code and provide a more meaningful UX for error # reporting - if self.failure and isinstance(self.failure, - BuildEnvironmentException): + if self.failure and isinstance( + self.failure, + BuildEnvironmentException, + ): self.build['exit_code'] = self.failure.status_code elif self.commands: - self.build['exit_code'] = max([cmd.exit_code - for cmd in self.commands]) + self.build['exit_code'] = max([ + cmd.exit_code for cmd in self.commands + ]) - self.build['setup'] = self.build['setup_error'] = "" - self.build['output'] = self.build['error'] = "" + self.build['setup'] = self.build['setup_error'] = '' + self.build['output'] = self.build['error'] = '' if self.start_time: build_length = (datetime.utcnow() - self.start_time) @@ -546,9 +588,13 @@ def update_build(self, state=None): if self.failure is not None: # Surface a generic error if the class is not a # BuildEnvironmentError - if not isinstance(self.failure, - (BuildEnvironmentException, - BuildEnvironmentWarning)): + if not isinstance( + self.failure, + ( + BuildEnvironmentException, + BuildEnvironmentWarning, + ), + ): log.error( 'Build failed with unhandled exception: %s', str(self.failure), @@ -564,7 +610,7 @@ def update_build(self, state=None): self.failure = BuildEnvironmentError( BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format( build_id=self.build['id'], - ) + ), ) self.build['error'] = str(self.failure) @@ -587,11 +633,11 @@ def update_build(self, state=None): api_v2.build(self.build['id']).put(self.build) except HttpClientError as e: log.exception( - "Unable to update build: id=%d", + 'Unable to update build: id=%d', self.build['id'], ) except Exception: - log.exception("Unknown build exception") + log.exception('Unknown build exception') class LocalBuildEnvironment(BuildEnvironment): @@ -632,7 +678,7 @@ def __init__(self, *args, **kwargs): build=self.build.get('id'), project_id=self.project.pk, project_name=self.project.slug, - )[:DOCKER_HOSTNAME_MAX_LEN] + )[:DOCKER_HOSTNAME_MAX_LEN], ) if self.config and self.config.build_image: self.container_image = self.config.build_image @@ -654,18 +700,25 @@ def __enter__(self): if state is not None: if state.get('Running') is True: exc = BuildEnvironmentError( - _('A build environment is currently ' - 'running for this version')) + _( + 'A build environment is currently ' + 'running for this version', + ), + ) self.failure = exc self.build['state'] = BUILD_STATE_FINISHED raise exc else: - log.warning(LOG_TEMPLATE - .format( - project=self.project.slug, - version=self.version.slug, - msg=("Removing stale container {0}" - .format(self.container_id)))) + log.warning( + LOG_TEMPLATE.format( + project=self.project.slug, + version=self.version.slug, + msg=( + 'Removing stale container {0}' + .format(self.container_id) + ), + ) + ) client = self.get_client() client.remove_container(self.container_id) except (DockerAPIError, ConnectionError): @@ -710,8 +763,7 @@ def __exit__(self, exc_type, exc_value, tb): # request. These errors should not surface to the user. except (DockerAPIError, ConnectionError): log.exception( - LOG_TEMPLATE - .format( + LOG_TEMPLATE.format( project=self.project.slug, version=self.version.slug, msg="Couldn't remove container", @@ -726,13 +778,7 @@ def __exit__(self, exc_type, exc_value, tb): if not all([exc_type, exc_value, tb]): exc_type, exc_value, tb = sys.exc_info() - ret = self.handle_exception(exc_type, exc_value, tb) - self.update_build(BUILD_STATE_FINISHED) - log.info(LOG_TEMPLATE - .format(project=self.project.slug, - version=self.version.slug, - msg='Build finished')) - return ret + return super(DockerBuildEnvironment, self).__exit__(exc_type, exc_value, tb) def get_client(self): """Create Docker client connection.""" @@ -757,7 +803,7 @@ def get_client(self): raise BuildEnvironmentError( BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format( build_id=self.build['id'], - ) + ), ) def get_container_host_config(self): @@ -836,14 +882,18 @@ def update_build_from_container_state(self): if state is not None and state.get('Running') is False: if state.get('ExitCode') == DOCKER_TIMEOUT_EXIT_CODE: self.failure = BuildEnvironmentError( - _('Build exited due to time out')) + _('Build exited due to time out'), + ) elif state.get('OOMKilled', False): self.failure = BuildEnvironmentError( - _('Build exited due to excessive memory consumption')) + _('Build exited due to excessive memory consumption'), + ) elif state.get('Error'): - self.failure = BuildEnvironmentError( - (_('Build exited due to unknown error: {0}') - .format(state.get('Error')))) + self.failure = BuildEnvironmentError(( + _('Build exited due to unknown error: {0}') + .format(state.get('Error')) + ), + ) def create_container(self): """Create docker container.""" @@ -855,9 +905,12 @@ def create_container(self): ) self.container = client.create_container( image=self.container_image, - command=('/bin/sh -c "sleep {time}; exit {exit}"' - .format(time=self.container_time_limit, - exit=DOCKER_TIMEOUT_EXIT_CODE)), + command=( + '/bin/sh -c "sleep {time}; exit {exit}"'.format( + time=self.container_time_limit, + exit=DOCKER_TIMEOUT_EXIT_CODE, + ) + ), name=self.container_id, hostname=self.container_id, host_config=self.get_container_host_config(), @@ -882,12 +935,11 @@ def create_container(self): raise BuildEnvironmentError( BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format( build_id=self.build['id'], - ) + ), ) except DockerAPIError as e: log.exception( - LOG_TEMPLATE - .format( + LOG_TEMPLATE.format( project=self.project.slug, version=self.version.slug, msg=e.explanation, diff --git a/readthedocs/doc_builder/exceptions.py b/readthedocs/doc_builder/exceptions.py index 78a62be6c45..360e2845256 100644 --- a/readthedocs/doc_builder/exceptions.py +++ b/readthedocs/doc_builder/exceptions.py @@ -1,14 +1,18 @@ +# -*- coding: utf-8 -*- """Exceptions raised when building documentation.""" +from __future__ import division, print_function, unicode_literals + from django.utils.translation import ugettext_noop class BuildEnvironmentException(Exception): message = None + status_code = None def __init__(self, message=None, **kwargs): - self.status_code = kwargs.pop('status_code', 1) + self.status_code = kwargs.pop('status_code', None) or self.status_code or 1 message = message or self.get_default_message() super(BuildEnvironmentException, self).__init__(message, **kwargs) @@ -19,17 +23,38 @@ def get_default_message(self): class BuildEnvironmentError(BuildEnvironmentException): GENERIC_WITH_BUILD_ID = ugettext_noop( - "There was a problem with Read the Docs while building your documentation. " - "Please report this to us with your build id ({build_id})." + 'There was a problem with Read the Docs while building your documentation. ' + 'Please report this to us with your build id ({build_id}).', ) class BuildEnvironmentCreationFailed(BuildEnvironmentError): - message = ugettext_noop( - "Build environment creation failed" + message = ugettext_noop('Build environment creation failed') + + +class VersionLockedError(BuildEnvironmentError): + + message = ugettext_noop('Version locked, retrying in 5 minutes.') + status_code = 423 + + +class ProjectBuildsSkippedError(BuildEnvironmentError): + + message = ugettext_noop('Builds for this project are temporarily disabled') + + +class YAMLParseError(BuildEnvironmentError): + + GENERIC_WITH_PARSE_EXCEPTION = ugettext_noop( + 'Problem parsing YAML configuration. {exception}', ) +class BuildTimeoutError(BuildEnvironmentError): + + message = ugettext_noop('Build exited due to time out') + + class BuildEnvironmentWarning(BuildEnvironmentException): pass diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 871857b7e48..c41154c5f9a 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Tasks related to projects. @@ -25,29 +26,27 @@ from django.core.urlresolvers import reverse from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from readthedocs.config import ConfigError from slumber.exceptions import HttpClientError -from .constants import LOG_TEMPLATE -from .exceptions import RepositoryError -from .models import ImportedFile, Project, Domain, Feature -from .signals import before_vcs, after_vcs, before_build, after_build, files_changed from readthedocs.builds.constants import ( BUILD_STATE_BUILDING, BUILD_STATE_CLONING, BUILD_STATE_FINISHED, BUILD_STATE_INSTALLING, LATEST, LATEST_VERBOSE_NAME, STABLE_VERBOSE_NAME) from readthedocs.builds.models import APIVersion, Build, Version from readthedocs.builds.signals import build_complete from readthedocs.builds.syncers import Syncer +from readthedocs.config import ConfigError from readthedocs.core.resolver import resolve_path from readthedocs.core.symlink import PublicSymlink, PrivateSymlink from readthedocs.core.utils import send_email, broadcast from readthedocs.doc_builder.config import load_yaml_config from readthedocs.doc_builder.constants import DOCKER_LIMITS -from readthedocs.doc_builder.environments import (LocalBuildEnvironment, - DockerBuildEnvironment) -from readthedocs.doc_builder.exceptions import BuildEnvironmentError +from readthedocs.doc_builder.environments import ( + DockerBuildEnvironment, LocalBuildEnvironment) +from readthedocs.doc_builder.exceptions import ( + BuildEnvironmentError, BuildTimeoutError, ProjectBuildsSkippedError, + VersionLockedError, YAMLParseError) from readthedocs.doc_builder.loader import get_builder_class -from readthedocs.doc_builder.python_environments import Virtualenv, Conda +from readthedocs.doc_builder.python_environments import Conda, Virtualenv from readthedocs.projects.models import APIProject from readthedocs.restapi.client import api as api_v2 from readthedocs.restapi.utils import index_search_request @@ -55,6 +54,11 @@ from readthedocs.vcs_support import utils as vcs_support_utils from readthedocs.worker import app +from .constants import LOG_TEMPLATE +from .exceptions import RepositoryError +from .models import Domain, Feature, ImportedFile, Project +from .signals import ( + after_build, after_vcs, before_build, before_vcs, files_changed) log = logging.getLogger(__name__) @@ -409,23 +413,19 @@ def run_setup(self, record=True): # Environment used for code checkout & initial configuration reading with self.setup_env: if self.project.skip: - raise BuildEnvironmentError( - _('Builds for this project are temporarily disabled')) + raise ProjectBuildsSkippedError try: self.setup_vcs() except vcs_support_utils.LockTimeout as e: self.task.retry(exc=e, throw=False) - raise BuildEnvironmentError( - 'Version locked, retrying in 5 minutes.', - status_code=423 - ) - + raise VersionLockedError try: self.config = load_yaml_config(version=self.version) except ConfigError as e: - raise BuildEnvironmentError( - 'Problem parsing YAML configuration. {0}'.format(str(e)) - ) + raise YAMLParseError( + YAMLParseError.GENERIC_WITH_PARSE_EXCEPTION.format( + exception=str(e), + )) if self.setup_env.failure or self.config is None: self._log('Failing build because of setup failure: %s' % self.setup_env.failure) @@ -459,8 +459,14 @@ def run_build(self, docker, record): env_cls = DockerBuildEnvironment else: env_cls = LocalBuildEnvironment - self.build_env = env_cls(project=self.project, version=self.version, config=self.config, - build=self.build, record=record, environment=env_vars) + self.build_env = env_cls( + project=self.project, + version=self.version, + config=self.config, + build=self.build, + record=record, + environment=env_vars, + ) # Environment used for building code, usually with Docker with self.build_env: @@ -472,9 +478,11 @@ def run_build(self, docker, record): if self.config.use_conda: self._log('Using conda') python_env_cls = Conda - self.python_env = python_env_cls(version=self.version, - build_env=self.build_env, - config=self.config) + self.python_env = python_env_cls( + version=self.version, + build_env=self.build_env, + config=self.config, + ) try: self.setup_python_environment() @@ -485,12 +493,9 @@ def run_build(self, docker, record): build_id = self.build.get('id') except vcs_support_utils.LockTimeout as e: self.task.retry(exc=e, throw=False) - raise BuildEnvironmentError( - 'Version locked, retrying in 5 minutes.', - status_code=423 - ) + raise VersionLockedError except SoftTimeLimitExceeded: - raise BuildEnvironmentError(_('Build exited due to time out')) + raise BuildTimeoutError # Finalize build and update web servers if build_id: @@ -713,11 +718,11 @@ def build_docs_search(self): """ Build search data with separate build. - Unless the project has the feature to allow - building the JSON search artifacts in the html build step. + Unless the project has the feature to allow building the JSON search + artifacts in the html build step. """ build_json_in_html_builder = self.project.has_feature( - Feature.BUILD_JSON_ARTIFACTS_WITH_HTML + Feature.BUILD_JSON_ARTIFACTS_WITH_HTML, ) if self.build_search and build_json_in_html_builder: # Already built in the html step @@ -760,7 +765,10 @@ def build_docs_class(self, builder_class): only raise a warning exception here. A hard error will halt the build process. """ - builder = get_builder_class(builder_class)(self.build_env, python_env=self.python_env) + builder = get_builder_class(builder_class)( + self.build_env, + python_env=self.python_env + ) success = builder.build() builder.move() return success @@ -835,42 +843,69 @@ def move_files(version_pk, hostname, html=False, localmedia=False, search=False, :type epub: bool """ version = Version.objects.get(pk=version_pk) - log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, - msg='Moving files')) + log.debug( + LOG_TEMPLATE.format( + project=version.project.slug, + version=version.slug, + msg='Moving files', + ) + ) if html: from_path = version.project.artifact_path( - version=version.slug, type_=version.project.documentation_type) + version=version.slug, + type_=version.project.documentation_type, + ) target = version.project.rtd_build_path(version.slug) Syncer.copy(from_path, target, host=hostname) if 'sphinx' in version.project.documentation_type: if search: from_path = version.project.artifact_path( - version=version.slug, type_='sphinx_search') + version=version.slug, + type_='sphinx_search', + ) to_path = version.project.get_production_media_path( - type_='json', version_slug=version.slug, include_file=False) + type_='json', + version_slug=version.slug, + include_file=False, + ) Syncer.copy(from_path, to_path, host=hostname) if localmedia: from_path = version.project.artifact_path( - version=version.slug, type_='sphinx_localmedia') + version=version.slug, + type_='sphinx_localmedia', + ) to_path = version.project.get_production_media_path( - type_='htmlzip', version_slug=version.slug, include_file=False) + type_='htmlzip', + version_slug=version.slug, + include_file=False, + ) Syncer.copy(from_path, to_path, host=hostname) # Always move PDF's because the return code lies. if pdf: - from_path = version.project.artifact_path(version=version.slug, - type_='sphinx_pdf') + from_path = version.project.artifact_path( + version=version.slug, + type_='sphinx_pdf', + ) to_path = version.project.get_production_media_path( - type_='pdf', version_slug=version.slug, include_file=False) + type_='pdf', + version_slug=version.slug, + include_file=False, + ) Syncer.copy(from_path, to_path, host=hostname) if epub: - from_path = version.project.artifact_path(version=version.slug, - type_='sphinx_epub') + from_path = version.project.artifact_path( + version=version.slug, + type_='sphinx_epub', + ) to_path = version.project.get_production_media_path( - type_='epub', version_slug=version.slug, include_file=False) + type_='epub', + version_slug=version.slug, + include_file=False, + ) Syncer.copy(from_path, to_path, host=hostname) @@ -975,22 +1010,36 @@ def fileify(version_pk, commit): project = version.project if not commit: - log.info(LOG_TEMPLATE - .format(project=project.slug, version=version.slug, - msg=('Imported File not being built because no commit ' - 'information'))) + log.info( + LOG_TEMPLATE.format( + project=project.slug, + version=version.slug, + msg=( + 'Imported File not being built because no commit ' + 'information' + ), + ) + ) return path = project.rtd_build_path(version.slug) if path: - log.info(LOG_TEMPLATE - .format(project=version.project.slug, version=version.slug, - msg='Creating ImportedFiles')) + log.info( + LOG_TEMPLATE.format( + project=version.project.slug, + version=version.slug, + msg='Creating ImportedFiles', + ) + ) _manage_imported_files(version, path, commit) else: - log.info(LOG_TEMPLATE - .format(project=project.slug, version=version.slug, - msg='No ImportedFile files')) + log.info( + LOG_TEMPLATE.format( + project=project.slug, + version=version.slug, + msg='No ImportedFile files', + ) + ) def _manage_imported_files(version, path, commit): @@ -1056,8 +1105,13 @@ def email_notification(version, build, email): :param build: :py:class:`Build` instance that failed :param email: Email recipient address """ - log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, - msg='sending email to: %s' % email)) + log.debug( + LOG_TEMPLATE.format( + project=version.project.slug, + version=version.slug, + msg='sending email to: %s' % email, + ) + ) # We send only what we need from the Django model objects here to avoid # serialization problems in the ``readthedocs.core.tasks.send_email_task`` @@ -1113,11 +1167,15 @@ def webhook_notification(version, build, hook_url): 'id': build.id, 'success': build.success, 'date': build.date.strftime('%Y-%m-%d %H:%M:%S'), - } + }, }) - log.debug(LOG_TEMPLATE - .format(project=project.slug, version='', - msg='sending notification to: %s' % hook_url)) + log.debug( + LOG_TEMPLATE.format( + project=project.slug, + version='', + msg='sending notification to: %s' % hook_url, + ) + ) try: requests.post(hook_url, data=data) except Exception: @@ -1144,11 +1202,13 @@ def update_static_metadata(project_pk, path=None): if not path: path = project.static_metadata_path() - log.info(LOG_TEMPLATE.format( - project=project.slug, - version='', - msg='Updating static metadata', - )) + log.info( + LOG_TEMPLATE.format( + project=project.slug, + version='', + msg='Updating static metadata', + ) + ) translations = [trans.language for trans in project.translations.all()] languages = set(translations) # Convert to JSON safe types @@ -1165,11 +1225,13 @@ def update_static_metadata(project_pk, path=None): json.dump(metadata, fh) fh.close() except (AttributeError, IOError) as e: - log.debug(LOG_TEMPLATE.format( - project=project.slug, - version='', - msg='Cannot write to metadata.json: {0}'.format(e) - )) + log.debug( + LOG_TEMPLATE.format( + project=project.slug, + version='', + msg='Cannot write to metadata.json: {0}'.format(e), + ) + ) # Random Tasks @@ -1181,7 +1243,7 @@ def remove_dir(path): This is mainly a wrapper around shutil.rmtree so that app servers can kill things on the build server. """ - log.info("Removing %s", path) + log.info('Removing %s', path) shutil.rmtree(path, ignore_errors=True) @@ -1231,7 +1293,8 @@ def finish_inactive_builds(): if build.project.container_time_limit: custom_delta = datetime.timedelta( - seconds=int(build.project.container_time_limit)) + seconds=int(build.project.container_time_limit), + ) if build.date + custom_delta > datetime.datetime.now(): # Do not mark as FINISHED builds with a custom time limit that wasn't # expired yet (they are still building the project version) @@ -1242,7 +1305,7 @@ def finish_inactive_builds(): build.error = _( 'This build was terminated due to inactivity. If you ' 'continue to encounter this error, file a support ' - 'request with and reference this build id ({0}).'.format(build.pk) + 'request with and reference this build id ({0}).'.format(build.pk), ) build.save() builds_finished += 1