docs: def download(auth, label, scan_ids=None, project=None, aid=None,
               out_dir='.', in_mem=True, progress=False, attempts=1):
      '''
      Download scan data from XNAT.
      
      Example:
          >>> import yaxil
          >>> auth = yaxil.XnatAuth(url='...', username='...', password='...')
          >>> yaxil.download(auth, 'AB1234C', ['1', '2'], out_dir='./data')
  
      :param auth: XNAT authentication object
      :type auth: :mod:`yaxil.XnatAuth`
      :param label: XNAT MR Session label
      :type label: str
      :param scan_ids: Scan numbers to return; use None for all
      :type scan_ids: list
      :param project: XNAT MR Session project
      :type project: str
      :param aid: XNAT Accession ID
      :type aid: str
      :param out_dir: Output directory
      :type out_dir: str
      :param in_mem: Keep download content in memory; faster but uses more memory
      :type in_mem: bool
      :param progress: Show download progress every N bytes
      :type progress: int
      :param attempts: Number of download attempts
      :type attempts: int
      ''' 
      if not scan_ids:
          scan_ids = ['ALL']
      if not aid:
          aid = accession(auth, label, project)
      # build the url
      url = "%s/data/experiments/%s/scans/%s/files?format=zip" % (auth.url.rstrip('/'), 
              aid, ','.join([str(x) for x in scan_ids]))
      # issue the http request, with exponential backoff retry behavior
      backoff = 10
      for _ in range(attempts): 
          logger.debug("issuing http request %s", url)
          r = requests.get(url, stream=True, auth=(auth.username, auth.password), verify=CHECK_CERTIFICATE)
          logger.debug("response headers %s", r.headers)
          if r.status_code == requests.codes.ok:
              break
          fuzz = random.randint(0, 10)
          logger.warn("download unsuccessful (%s), retrying in %s seconds", r.status_code, 
                      backoff + fuzz)
          time.sleep(backoff + fuzz)
          backoff *= 2
      # if we still have a not-ok status at this point, the download failed
      if r.status_code != requests.codes.ok:
          raise DownloadError("response not ok (%s) from %s" % (r.status_code, r.url))
      # create output directory
      if not os.path.exists(out_dir):
          os.makedirs(out_dir)
      # keep response content in memory or write to a file (memory is obviously faster, but limited)
      if in_mem:
          content = io.BytesIO()
          logger.debug("response content will be read into memory")
      else:
          content = tf.NamedTemporaryFile(dir=out_dir, prefix="xnat", suffix=".zip")
          logger.debug("response content will be stored on disk %s", content.name)
      # progress indicator setup
      if progress:
          sys.stdout.write("reading response data: ")
          sys.stdout.flush()
      # read response content in chunks
      meter = 0
      chunk_size = 1024
      for chunk in r.iter_content(chunk_size=chunk_size):
          if progress and meter >= progress:
              sys.stdout.write(next(commons.spinner)); sys.stdout.flush()
              sys.stdout.write('\b')
              meter = 0
          content.write(chunk)
          meter += chunk_size
      # progress indicator shut down
      if progress:
          sys.stdout.write('done.\n'); sys.stdout.flush()
      # load reponse content into a zipfile object
      try:
          zf = zipfile.ZipFile(content, allowZip64=True)
      except zipfile.BadZipfile:
          with tf.NamedTemporaryFile(dir=out_dir, prefix="xnat",
                                     suffix=".zip", delete=False) as fo:
              content.seek(0)
              fo.write(content.read())
          raise DownloadError("bad zip file, written to %s" % fo.name)
      # finally extract the zipfile (with various nasty edge cases handled)
      logger.debug("extracting zip archive to %s", out_dir)
      extract(zf, content, out_dir)