text: import logging
  import json
  
  from datetime import datetime, timedelta
  
  from flask import request
  
  from app import app, dockerfile_build_queue
  from data import model
  from data.logs_model import logs_model
  from data.database import db, RepositoryState
  from auth.auth_context import get_authenticated_user
  from notifications import spawn_notification
  from util.names import escape_tag
  from util.morecollections import AttrDict
  from util.request import get_request_ip
  
  
  logger = logging.getLogger(__name__)
  
  
  class MaximumBuildsQueuedException(Exception):
      """
      This exception is raised when a build is requested, but the incoming build would exceed the
      configured maximum build rate.
      """
  
      pass
  
  
  class BuildTriggerDisabledException(Exception):
      """
      This exception is raised when a build is required, but the build trigger has been disabled.
      """
  
      pass
  
  
  def start_build(repository, prepared_build, pull_robot_name=None):
      # Ensure that builds are only run in image repositories.
      if repository.kind.name != "image":
          raise Exception("Attempt to start a build for application repository %s" % repository.id)
  
      # Ensure the repository isn't in mirror or read-only mode.
      if repository.state != RepositoryState.NORMAL:
          raise Exception(
              (
                  "Attempt to start a build for a non-normal repository: %s %s"
                  % (repository.id, repository.state)
              )
          )
  
      # Ensure that disabled triggers are not run.
      if prepared_build.trigger is not None and not prepared_build.trigger.enabled:
          raise BuildTriggerDisabledException
  
      if repository.namespace_user.maximum_queued_builds_count is not None:
          queue_item_canonical_name = [repository.namespace_user.username]
          alive_builds = dockerfile_build_queue.num_alive_jobs(queue_item_canonical_name)
          if alive_builds >= repository.namespace_user.maximum_queued_builds_count:
              logger.debug(
                  "Prevented queueing of build under namespace %s due to reaching max: %s",
                  repository.namespace_user.username,
                  repository.namespace_user.maximum_queued_builds_count,
              )
              raise MaximumBuildsQueuedException()
  
      host = app.config["SERVER_HOSTNAME"]
      repo_path = "%s/%s/%s" % (host, repository.namespace_user.username, repository.name)
  
      new_token = model.token.create_access_token(
          repository, "write", kind="build-worker", friendly_name="Repository Build Token"
      )
      logger.debug(
          "Creating build %s with repo %s tags %s",
          prepared_build.build_name,
          repo_path,
          prepared_build.tags,
      )
  
      job_config = {
          "docker_tags": prepared_build.tags,
          "registry": host,
          "build_subdir": prepared_build.subdirectory,
          "context": prepared_build.context,
          "trigger_metadata": prepared_build.metadata or {},
          "is_manual": prepared_build.is_manual,
          "manual_user": get_authenticated_user().username if get_authenticated_user() else None,
          "archive_url": prepared_build.archive_url,
      }
  
      with app.config["DB_TRANSACTION_FACTORY"](db):
          build_request = model.build.create_repository_build(
              repository,
              new_token,
              job_config,
              prepared_build.dockerfile_id,
              prepared_build.build_name,
              prepared_build.trigger,
              pull_robot_name=pull_robot_name,
          )
  
          pull_creds = model.user.get_pull_credentials(pull_robot_name) if pull_robot_name else None
  
          json_data = json.dumps({"build_uuid": build_request.uuid, "pull_credentials": pull_creds})
  
          queue_id = dockerfile_build_queue.put(
              [repository.namespace_user.username, repository.name], json_data, retries_remaining=3
          )
  
          build_request.queue_id = queue_id
          build_request.save()
  
      # Add the build to the repo's log and spawn the build_queued notification.
      event_log_metadata = {
          "build_id": build_request.uuid,
          "docker_tags": prepared_build.tags,
          "repo": repository.name,
          "namespace": repository.namespace_user.username,
          "is_manual": prepared_build.is_manual,
          "manual_user": get_authenticated_user().username if get_authenticated_user() else None,
      }
  
      if prepared_build.trigger:
          event_log_metadata["trigger_id"] = prepared_build.trigger.uuid
          event_log_metadata["trigger_kind"] = prepared_build.trigger.service.name
          event_log_metadata["trigger_metadata"] = prepared_build.metadata or {}
  
      logs_model.log_action(
          "build_dockerfile",
          repository.namespace_user.username,
          ip=get_request_ip(),
          metadata=event_log_metadata,
          repository=repository,
      )
  
      # TODO: remove when more endpoints have been converted to using interfaces
      repo = AttrDict(
          {
              "namespace_name": repository.namespace_user.username,
              "name": repository.name,
          }
      )
  
      spawn_notification(
          repo,
          "build_queued",
          event_log_metadata,
          subpage="build/%s" % build_request.uuid,
          pathargs=["build", build_request.uuid],
      )
  
      return build_request
  
  
  class PreparedBuild(object):
      """
      Class which holds all the information about a prepared build.
  
      The build queuing service will use this result to actually invoke the build.
      """
  
      def __init__(self, trigger=None):
          self._dockerfile_id = None
          self._archive_url = None
          self._tags = None
          self._build_name = None
          self._subdirectory = None
          self._context = None
          self._metadata = None
          self._trigger = trigger
          self._is_manual = None
  
      @staticmethod
      def get_display_name(sha):
          return sha[0:7]
  
      def name_from_sha(self, sha):
          self.build_name = PreparedBuild.get_display_name(sha)
  
      @property
      def is_manual(self):
          if self._is_manual is None:
              raise Exception("Property is_manual not set")
  
          return self._is_manual
  
      @is_manual.setter
      def is_manual(self, value):
          if self._is_manual is not None:
              raise Exception("Property is_manual already set")
  
          self._is_manual = value
  
      @property
      def trigger(self):
          return self._trigger
  
      @property
      def archive_url(self):
          return self._archive_url
  
      @archive_url.setter
      def archive_url(self, value):
          if self._archive_url:
              raise Exception("Property archive_url already set")
  
          self._archive_url = value
  
      @property
      def dockerfile_id(self):
          return self._dockerfile_id
  
      @dockerfile_id.setter
      def dockerfile_id(self, value):
          if self._dockerfile_id:
              raise Exception("Property dockerfile_id already set")
  
          self._dockerfile_id = value
  
      @property
      def tags(self):
          if not self._tags:
              raise Exception("Missing property tags")
  
          return self._tags
  
      @tags.setter
      def tags(self, value):
          if self._tags:
              raise Exception("Property tags already set")
  
          self._tags = [escape_tag(tag, default="latest") for tag in value]
  
      @property
      def build_name(self):
          if not self._build_name:
              raise Exception("Missing property build_name")
  
          return self._build_name
  
      @build_name.setter
      def build_name(self, value):
          if self._build_name:
              raise Exception("Property build_name already set")
  
          self._build_name = value
  
      @property
      def subdirectory(self):
          if self._subdirectory is None:
              raise Exception("Missing property subdirectory")
  
          return self._subdirectory
  
      @subdirectory.setter
      def subdirectory(self, value):
          if self._subdirectory:
              raise Exception("Property subdirectory already set")
  
          self._subdirectory = value
  
      @property
      def context(self):
          if self._context is None:
              raise Exception("Missing property context")
  
          return self._context
  
      @context.setter
      def context(self, value):
          if self._context:
              raise Exception("Property context already set")
  
          self._context = value
  
      @property
      def metadata(self):
          if self._metadata is None:
              raise Exception("Missing property metadata")
  
          return self._metadata
  
      @metadata.setter
      def metadata(self, value):
          if self._metadata:
              raise Exception("Property metadata already set")
  
          self._metadata = value
  