text: """Module provider for UKFast's SafeDNS"""
  import json
  import logging
  
  import requests
  
  from lexicon.exceptions import AuthenticationError
  from lexicon.providers.base import Provider as BaseProvider
  
  LOGGER = logging.getLogger(__name__)
  
  NAMESERVER_DOMAINS = ["ukfast.net"]
  
  
  def provider_parser(subparser):
      """Configure provider parser for SafeDNS"""
      subparser.description = """
          SafeDNS provider requires an API key in all interactions.
          You can generate one for your account on the following URL:
          https://my.ukfast.co.uk/applications/index.php"""
      subparser.add_argument(
          "--auth-token", help="specify the API key to authenticate with"
      )
  
  
  class Provider(BaseProvider):
      """Provider SafeDNS implementation of Lexicon Provider interface."""
  
      def __init__(self, config):
          super(Provider, self).__init__(config)
          self.domain_id = None
          self.api_endpoint = "https://api.ukfast.io/safedns/v1"
  
      def _authenticate(self):
          try:
              self._get(f"/zones/{self.domain}")
              self.domain_id = self.domain
          except requests.exceptions.HTTPError as err:
              if err.response.status_code == 404:
                  raise AuthenticationError("No domain found")
              raise err
  
      # List all records. Return an empty list if no records found.
      # type, name and content are used to filter records.
      def _list_records(self, rtype=None, name=None, content=None):
          url = f"/zones/{self.domain_id}/records"
          records = []
          payload = {}
  
          # Handle pagination and iteratively collect all records
          next_url = url
          while next_url is not None:
              payload = self._get(next_url)
              if (
                  "meta" in payload
                  and "pagination" in payload["meta"]
                  and "links" in payload["meta"]["pagination"]
                  and "next" in payload["meta"]["pagination"]["links"]
              ):
                  next_url = payload["meta"]["pagination"]["links"]["next"]
              else:
                  next_url = None
  
              # Assign the returned attributes to the keys that lexicon expects
              for record in payload["data"]:
                  record = self._clean_TXT_record(record)
                  processed_record = {
                      "id": record["id"],
                      "name": record["name"],
                      "type": record["type"],
                      "content": record["content"],
                      "updated_at": record["updated_at"],
                      "ttl": record["ttl"],
                      "priority": record["priority"],
                  }
                  records.append(processed_record)
  
          # This is filtering logic to return only the record which matches what has
          # been passed in to the method
          if rtype:
              records = [record for record in records if record["type"] == rtype]
          if name:
              records = [
                  record
                  for record in records
                  # DNS should not be case-sensitive, so perform lower-case comparison
                  if record["name"].lower() == self._full_name(name).lower()
              ]
          if content:
              records = [
                  record
                  for record in records
                  if record["content"].lower() == content.lower()
              ]
  
          LOGGER.debug("list_records: %s", records)
          return records
  
      def _create_record(self, rtype, name, content):
  
          # Check whether the record already exists with the same  rtype, name & content.
          # If so, claim to have added the record, but dont't do anything.
          records = self._list_records(rtype, name, content)
          if records:
              LOGGER.debug(
                  "create_record: (ignored, duplicate record): %s", records[0]["id"]
              )
              return True
  
          # Make sure TXT records are wrapped in quotes
          if content:
              content = self._add_quotes(rtype, content)
  
          data = {"name": self._full_name(name), "type": rtype, "content": content}
  
          self._post(f"/zones/{self.domain}/records", data)
          LOGGER.debug("create_record: %s", True)
          return True
  
      def _update_record(self, identifier, rtype=None, name=None, content=None):
  
          # Make sure the update won't cause a duplicate entry. If it will, fail silently
          records = self._list_records(rtype, name, content)
  
          if not identifier:
              records = self._list_records(rtype, name)
              if len(records) == 1:
                  identifier = records[0]["id"]
              elif len(records) > 1:
                  identifier = records[0]["id"]
                  LOGGER.warning(
                      "Warning, multiple records found for given parameters, "
                      "only first one will be updated: %s",
                      records,
                  )
              else:
                  raise Exception("Record identifier could not be found")
  
          # Make sure TXT records are wrapped in quotes
          if content:
              content = self._add_quotes(rtype, content)
  
          data = {}
          if name:
              data["name"] = self._full_name(name)
          if rtype:
              data["type"] = rtype
          if content:
              data["content"] = content
  
          self._patch(f"/zones/{self.domain}/records/{identifier}", data)
  
          LOGGER.debug("update_record: %s", True)
          return True
  
      def _delete_record(self, identifier=None, rtype=None, name=None, content=None):
  
          delete_record_ids = []
  
          # If we've not been given an identifier, search for matching records.
          # NOTE, this could cause multiple records to be removed.
          if not identifier:
              records = self._list_records(rtype, name, content)
              delete_record_ids = [record["id"] for record in records]
          else:
              delete_record_ids.append(identifier)
  
          LOGGER.debug("delete_records: %s", delete_record_ids)
  
          for delete_record_id in delete_record_ids:
              self._delete(f"/zones/{self.domain}/records/{delete_record_id}")
  
          LOGGER.debug("delete_record: %s", True)
          return True
  
      def _request(self, action="GET", url="/", data=None, query_params=None):
          if data is None:
              data = {}
          if query_params is None:
              query_params = {}
          default_headers = {
              "Accept": "application/json",
              "Content-Type": "application/json",
              "Authorization": f"{self._get_provider_option('auth_token')}",
          }
          if not url.startswith(self.api_endpoint):
              url = self.api_endpoint + url
  
          response = requests.request(
              action,
              url,
              params=query_params,
              data=json.dumps(data),
              headers=default_headers,
          )
  
          # Sort this out to work properly, writing errors to the correct place
          if "content" in response:
              resp = json.loads(response.content)
              if "errors" in resp:
                  for error in resp["errors"]:
                      print("ERROR: " + error["detail"])
  
          # If the request fails for any reason, throw an error.
          response.raise_for_status()
  
          # There is no JSON returned when calling with DELETE
          if action != "DELETE":
              return response.json()
  
          return True
  
      # The content of TXT entries must be quoted. This static method ensures that.
      @staticmethod
      def _add_quotes(rtype, content):
          if rtype == "TXT":
              if not content.startswith('"') and not content.endswith('"'):
                  return f'"{content}"'
          return content
  