def upload_sequence(
    files,
    session,
    samples_resource,
    metadata=None,
    tags=None,
    project=None,
    coerce_ascii=False,
    progressbar=None,
):
    """Uploads a sequence file (or pair of files) to the One Codex server via either our proxy or directly to S3.

    Parameters
    ----------
    files : `list`
        A list of paths to files on the system, or tuples containing pairs of paths. Tuples will be
        interleaved as paired-end reads and both files should contain the same number of records.
        Paths to single files will be uploaded as-is.
    session : `requests.Session`
        Connection to One Codex API.
    samples_resource : `onecodex.models.Samples`
        Wrapped potion-client object exposing `init_upload` and `confirm_upload` methods.
    metadata : `dict`, optional
    tags : `list`, optional
    project : `string`, optional
        UUID of project to associate this sample with.
    coerce_ascii : `bool`, optional
        If true, rename unicode filenames to ASCII and issue warning.
    progressbar : `click.progressbar`, optional
        If passed, display a progress bar using Click.

    Returns
    -------
    A Sample object for the completed upload.
    """
    filename, file_size, file_format = _file_stats(files)

    # if filename cannot be represented as ascii, raise and suggest renaming
    try:
        # python2
        ascii_fname = unidecode(unicode(filename))
    except NameError:
        ascii_fname = unidecode(filename)

    if filename != ascii_fname:
        if coerce_ascii:
            # TODO: Consider warnings.warn here instead
            logging.warn(
                "Renaming {} to {}, must be ASCII\n".format(filename.encode("utf-8"), ascii_fname)
            )
            filename = ascii_fname
        else:
            raise OneCodexException("Filenames must be ascii. Try using --coerce-ascii")

    # disable progressbar while keeping context manager
    if not progressbar:
        progressbar = FakeProgressBar()

    # file_path is the path to the file on this disk. file_name is what we'll call the file in the
    # mainline database. file_size is the sum of both files in a pair, or the size of an unpaired
    # file. if paired, file_size is the uncompressed size. if unpaired, file_size is the actual
    # size on disk. unpaired files are uploaded as-is. paired files are decompressed, interleaved,
    # and uploaded as uncompressed data.
    with progressbar as bar:
        if isinstance(files, tuple):
            fobj = FASTXInterleave(files, file_size, file_format, bar)
        else:
            fobj = FilePassthru(files, file_size, bar)

        # must call init_upload in this loop in order to get a sample uuid we can call
        # cancel_upload on later if user hits ctrl+c
        fields = _call_init_upload(filename, file_size, metadata, tags, project, samples_resource)

        def cancel_atexit():
            bar.canceled = True
            bar.update(1)
            logging.info("Canceled upload for sample: {}".format(fields["sample_id"]))
            samples_resource.cancel_upload({"sample_id": fields["sample_id"]})

        atexit_register(cancel_atexit)

        # if the upload via init_upload fails, upload_sequence_fileobj will call
        # init_multipart_upload, which accepts metadata to be integrated into a newly-created
        # Sample model. if the s3 intermediate route is used, two Sample models will ultimately
        # exist on mainline: the failed fastx-proxy upload and the successful s3 intermediate.
        retry_fields = _make_retry_fields(filename, metadata, tags, project)

        try:
            sample_id = upload_sequence_fileobj(
                fobj, filename, fields, retry_fields, session, samples_resource
            )
            atexit_unregister(cancel_atexit)
            return sample_id
        except KeyboardInterrupt:
            cancel_atexit()
            atexit_unregister(cancel_atexit)
            raise