diff --git a/ddsc/core/download.py b/ddsc/core/download.py
index f7d35b4..bd97279 100644
--- a/ddsc/core/download.py
+++ b/ddsc/core/download.py
@@ -7,7 +7,7 @@ import time
 import queue
 from ddsc.core.localstore import HashUtil
 from ddsc.core.ddsapi import DDS_TOTAL_HEADER
-from ddsc.core.util import humanize_bytes
+from ddsc.core.util import humanize_bytes, transfer_speed_str
 
 SWIFT_EXPIRED_STATUS_CODE = 401
 S3_EXPIRED_STATUS_CODE = 403
@@ -324,11 +324,11 @@ class ProjectFileDownloader(object):
         return self.spinner_chars[half_seconds % 4]
 
     def make_download_speed(self, current_time, total_bytes_downloaded):
-        elapsed_seconds = current_time - self.start_time
-        if elapsed_seconds > 0 and total_bytes_downloaded > 0:
-            bytes_per_second = float(total_bytes_downloaded) / (elapsed_seconds + 0.5)
-            return '@ {}/s'.format(humanize_bytes(bytes_per_second))
-        return ''
+        return transfer_speed_str(
+            current_time=current_time,
+            start_time=self.start_time,
+            transferred_bytes=total_bytes_downloaded
+        )
 
     def get_download_progress(self):
         files_downloaded = 0
diff --git a/ddsc/core/fileuploader.py b/ddsc/core/fileuploader.py
index 19e4941..ba342fe 100644
--- a/ddsc/core/fileuploader.py
+++ b/ddsc/core/fileuploader.py
@@ -403,7 +403,7 @@ class ChunkSender(object):
             while sent_chunks != self.num_chunks_to_send:
                 chunk = infile.read(self.chunk_size)
                 self._send_chunk(chunk, chunk_num)
-                self.progress_queue.processed(1)
+                self.progress_queue.processed((1, len(chunk)))
                 chunk_num += 1
                 sent_chunks += 1
 
diff --git a/ddsc/core/projectuploader.py b/ddsc/core/projectuploader.py
index 2e94b9e..15c2b53 100644
--- a/ddsc/core/projectuploader.py
+++ b/ddsc/core/projectuploader.py
@@ -453,7 +453,7 @@ class CreateSmallFileCommand(object):
             self.local_file.set_remote_values_after_send(remote_file_id,
                                                          remote_hash_dict['algorithm'],
                                                          remote_hash_dict['value'])
-            self.settings.watcher.transferring_item(self.local_file)
+            self.settings.watcher.transferring_item(self.local_file, transferred_bytes=self.local_file.size)
         else:
             self.settings.watcher.increment_progress()
 
diff --git a/ddsc/core/util.py b/ddsc/core/util.py
index c009d92..5b44779 100644
--- a/ddsc/core/util.py
+++ b/ddsc/core/util.py
@@ -2,6 +2,7 @@ import sys
 import os
 import platform
 import stat
+import time
 
 TERMINAL_ENCODING_NOT_UTF_ERROR = """
 ERROR: DukeDSClient requires UTF terminal encoding.
@@ -70,8 +71,10 @@ class ProgressPrinter(object):
         self.waiting = False
         self.msg_verb = msg_verb
         self.progress_bar = ProgressBar()
+        self.transferred_bytes = 0
+        self.total_bytes = 0
 
-    def transferring_item(self, item, increment_amt=1, override_msg_verb=None):
+    def transferring_item(self, item, increment_amt=1, override_msg_verb=None, transferred_bytes=0):
         """
         Update progress that item is about to be transferred.
         :param item: LocalFile, LocalFolder, or LocalContent(project) that is about to be sent.
@@ -87,7 +90,8 @@ class ProgressPrinter(object):
         msg_verb = self.msg_verb
         if override_msg_verb:
             msg_verb = override_msg_verb
-        self.progress_bar.update(percent_done, '{} {}'.format(msg_verb, details))
+        self.transferred_bytes += transferred_bytes
+        self.progress_bar.update(percent_done, self.transferred_bytes, '{} {}'.format(msg_verb, details))
         self.progress_bar.show()
 
     def increment_progress(self, amt=1):
@@ -138,21 +142,26 @@ class ProgressBar(object):
         self.line = ''
         self.state = self.STATE_RUNNING
         self.wait_msg = 'Waiting'
+        self.transferred_bytes = 0
+        self.start_time = time.time()
 
-    def update(self, percent_done, details):
+    def update(self, percent_done, transferred_bytes, details):
         self.percent_done = percent_done
         self.current_item_details = details
+        self.transferred_bytes = transferred_bytes
 
     def set_state(self, state):
         self.state = state
 
     def _get_line(self):
+        speed = transfer_speed_str(current_time=time.time(), start_time=self.start_time,
+                                   transferred_bytes=self.transferred_bytes)
         if self.state == self.STATE_DONE:
-            return 'Done: 100%'
+            return 'Done: 100%{}'.format(speed)
         details = self.current_item_details
         if self.state == self.STATE_WAITING:
             details = self.wait_msg
-        return 'Progress: {}% - {}'.format(self.percent_done, details)
+        return 'Progress: {}%{} - {}'.format(self.percent_done, speed, details)
 
     def show(self):
         line = self._get_line()
@@ -344,8 +353,8 @@ def wait_for_processes(processes, size, progress_queue, watcher, item):
     while size > 0:
         progress_type, value = progress_queue.get()
         if progress_type == ProgressQueue.PROCESSED:
-            chunk_size = value
-            watcher.transferring_item(item, increment_amt=chunk_size)
+            chunk_size, transferred_bytes = value
+            watcher.transferring_item(item, increment_amt=chunk_size, transferred_bytes=transferred_bytes)
             size -= chunk_size
         elif progress_type == ProgressQueue.START_WAITING:
             watcher.start_waiting()
@@ -451,3 +460,18 @@ def join_with_commas_and_and(items):
         return ' and '.join([head_items_str, last_item])
     else:
         return last_item
+
+
+def transfer_speed_str(current_time, start_time, transferred_bytes):
+    """
+    Return transfer speed str based
+    :param current_time: float: current time
+    :param start_time: float: starting time
+    :param transferred_bytes: int: bytes transferred
+    :return: str: end user str
+    """
+    elapsed_seconds = current_time - start_time
+    if elapsed_seconds > 0 and transferred_bytes > 0:
+        bytes_per_second = float(transferred_bytes) / (elapsed_seconds + 0.5)
+        return '@ {}/s'.format(humanize_bytes(bytes_per_second))
+    return ''

Include speed for uploads in progress bar
The downloads progress bar has been updated to include speed (MB/s) - PR #295
This has been useful allowing users to easily provide meaningful performance metrics.
The uploads progress bar should also include this information.
diff --git a/ddsc/core/tests/test_projectuploader.py b/ddsc/core/tests/test_projectuploader.py
index 7b35539..3c090f8 100644
--- a/ddsc/core/tests/test_projectuploader.py
+++ b/ddsc/core/tests/test_projectuploader.py
@@ -373,4 +373,4 @@ class TestCreateSmallFileCommand(TestCase):
         cmd.after_run(remote_file_data)
         cmd.file_upload_post_processor.run.assert_called_with(cmd.settings.data_service, remote_file_data)
         cmd.local_file.set_remote_values_after_send.assert_called_with('abc123', 'md5', 'abcdefg')
-        cmd.settings.watcher.transferring_item.assert_called_with(cmd.local_file)
+        cmd.settings.watcher.transferring_item.assert_called_with(cmd.local_file, transferred_bytes=cmd.local_file.size)
diff --git a/ddsc/core/tests/test_util.py b/ddsc/core/tests/test_util.py
index 2a04281..893d995 100644
--- a/ddsc/core/tests/test_util.py
+++ b/ddsc/core/tests/test_util.py
@@ -26,66 +26,70 @@ class TestUtil(TestCase):
 
 class TestProgressBar(TestCase):
     @patch('ddsc.core.util.sys.stdout')
-    def test_show_no_waiting(self, mock_stdout):
+    @patch('ddsc.core.util.transfer_speed_str')
+    def test_show_no_waiting(self, mock_transfer_speed_str, mock_stdout):
         progress_bar = ProgressBar()
+        mock_transfer_speed_str.return_value = ' @ 100 MB/s'
 
         # replace line with our progress
-        progress_bar.update(percent_done=0, details='sending really_long_filename.txt')
+        progress_bar.update(percent_done=0, details='sending really_long_filename.txt', transferred_bytes=0)
         progress_bar.show()
-        expected = '\rProgress: 0% - sending really_long_filename.txt'
+        expected = '\rProgress: 0% @ 100 MB/s - sending really_long_filename.txt'
         mock_stdout.write.assert_called_with(expected)
 
         # replace line with our progress (make sure it is long enough to blank out previous line)
-        progress_bar.update(percent_done=10, details='sending short.txt')
+        progress_bar.update(percent_done=10, details='sending short.txt', transferred_bytes=100)
         progress_bar.show()
-        expected = '\rProgress: 10% - sending short.txt              '
+        expected = '\rProgress: 10% @ 100 MB/s - sending short.txt              '
         mock_stdout.write.assert_called_with(expected)
 
-        progress_bar.update(percent_done=15, details='sending short.txt')
+        progress_bar.update(percent_done=15, details='sending short.txt', transferred_bytes=50)
         progress_bar.show()
-        expected = '\rProgress: 15% - sending short.txt              '
+        expected = '\rProgress: 15% @ 100 MB/s - sending short.txt              '
         mock_stdout.write.assert_called_with(expected)
 
         # we finish uploading(go to newline)
         progress_bar.set_state(ProgressBar.STATE_DONE)
         progress_bar.show()
-        expected = '\rDone: 100%                                     \n'
+        expected = '\rDone: 100% @ 100 MB/s                                     \n'
         mock_stdout.write.assert_called_with(expected)
 
     @patch('ddsc.core.util.sys.stdout')
-    def test_show_with_waiting(self, mock_stdout):
+    @patch('ddsc.core.util.transfer_speed_str')
+    def test_show_with_waiting(self, mock_transfer_speed_str, mock_stdout):
         progress_bar = ProgressBar()
+        mock_transfer_speed_str.return_value = ' @ 100 MB/s'
 
         # we make some progress
-        progress_bar.update(percent_done=10, details='sending short.txt')
+        progress_bar.update(percent_done=10, details='sending short.txt', transferred_bytes=100)
         progress_bar.show()
-        expected = '\rProgress: 10% - sending short.txt'
+        expected = '\rProgress: 10% @ 100 MB/s - sending short.txt'
         mock_stdout.write.assert_called_with(expected)
 
         # we get stuck waiting
         progress_bar.wait_msg = "Waiting for project"
         progress_bar.set_state(ProgressBar.STATE_WAITING)
         progress_bar.show()
-        expected = '\rProgress: 10% - Waiting for project'
+        expected = '\rProgress: 10% @ 100 MB/s - Waiting for project'
         mock_stdout.write.assert_called_with(expected)
 
         # waiting takes priority over progress updates
         # (we may be able to upload some folders while waiting to upload files)
-        progress_bar.update(percent_done=15, details='sending short.txt')
+        progress_bar.update(percent_done=15, details='sending short.txt', transferred_bytes=50)
         progress_bar.show()
-        expected = '\rProgress: 15% - Waiting for project'
+        expected = '\rProgress: 15% @ 100 MB/s - Waiting for project'
         mock_stdout.write.assert_called_with(expected)
 
         # we are not longer waiting
         progress_bar.set_state(ProgressBar.STATE_RUNNING)
         progress_bar.show()
-        expected = '\rProgress: 15% - sending short.txt  '
+        expected = '\rProgress: 15% @ 100 MB/s - sending short.txt  '
         mock_stdout.write.assert_called_with(expected)
 
         # we finish uploading(go to newline)
         progress_bar.set_state(ProgressBar.STATE_DONE)
         progress_bar.show()
-        expected = '\rDone: 100%                         \n'
+        expected = '\rDone: 100% @ 100 MB/s                         \n'
         mock_stdout.write.assert_called_with(expected)
 
     @patch('ddsc.core.util.sys.stdout')
@@ -111,20 +115,20 @@ class TestProgressBar(TestCase):
 
 class TestProgressPrinter(TestCase):
     @patch('ddsc.core.util.ProgressBar')
-    def test_stuff(self, mock_progress_bar):
+    def test_general_functionality(self, mock_progress_bar):
         progress_printer = ProgressPrinter(total=10, msg_verb='sending')
 
         # pretend we just created a project
         mock_project = Mock(kind=KindType.project_str, path='')
-        progress_printer.transferring_item(item=mock_project, increment_amt=1)
-        mock_progress_bar.return_value.update.assert_called_with(10, 'sending project')
+        progress_printer.transferring_item(item=mock_project, increment_amt=1, transferred_bytes=100)
+        mock_progress_bar.return_value.update.assert_called_with(10, 100, 'sending project')
         mock_progress_bar.return_value.show.assert_called()
         mock_progress_bar.reset_mock()
 
         # pretend we just created a folder
         mock_project = Mock(kind=KindType.folder_str, path='/data')
-        progress_printer.transferring_item(item=mock_project, increment_amt=2)
-        mock_progress_bar.return_value.update.assert_called_with(30, 'sending data')
+        progress_printer.transferring_item(item=mock_project, increment_amt=2, transferred_bytes=200)
+        mock_progress_bar.return_value.update.assert_called_with(30, 300, 'sending data')
         mock_progress_bar.return_value.show.assert_called()
         mock_progress_bar.reset_mock()
 
@@ -141,7 +145,7 @@ class TestProgressPrinter(TestCase):
         # pretend we uploaded a file
         mock_project = Mock(kind=KindType.file_str, path='/data/log.txt')
         progress_printer.transferring_item(item=mock_project, increment_amt=2)
-        mock_progress_bar.return_value.update.assert_called_with(50, 'sending log.txt')
+        mock_progress_bar.return_value.update.assert_called_with(50, 300, 'sending log.txt')
         mock_progress_bar.return_value.show.assert_called()
         mock_progress_bar.reset_mock()
 
@@ -181,10 +185,10 @@ class TestProgressPrinter(TestCase):
         mock_os.path.basename.return_value = 'somefile.txt'
         progress_printer = ProgressPrinter(total=10, msg_verb='sending')
         progress_printer.transferring_item(Mock(), increment_amt=0)
-        progress_bar.update.assert_called_with(0, "sending somefile.txt")
+        progress_bar.update.assert_called_with(0, 0, "sending somefile.txt")
         progress_bar.reset_mock()
         progress_printer.transferring_item(Mock(), increment_amt=0, override_msg_verb='checking')
-        progress_bar.update.assert_called_with(0, "checking somefile.txt")
+        progress_bar.update.assert_called_with(0, 0, "checking somefile.txt")
 
     def test_increment_progress(self):
         progress_printer = ProgressPrinter(total=10, msg_verb='sending')

pip install -e .[dev]
parse_log_pytest
requirements.txt
mock flake8 nose pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
-e git+https://github.com/Duke-GCB/DukeDSClient.git@259f337f4d2782eb9249473046d8dd59daf01533#egg=DukeDSClient
exceptiongroup==1.2.2
flake8==7.2.0
future==0.16.0
idna==3.10
iniconfig==2.1.0
mccabe==0.7.0
mock==5.2.0
nose==1.3.7
packaging==24.2
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.2
pytest==8.3.5
pytz==2025.2
PyYAML==5.1
requests==2.32.3
six==1.10.0
tenacity==5.0.4
tomli==2.2.1
urllib3==2.3.0

name: DukeDSClient
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - future==0.16.0
      - idna==3.10
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - mock==5.2.0
      - nose==1.3.7
      - packaging==24.2
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.2
      - pytest==8.3.5
      - pytz==2025.2
      - pyyaml==5.1
      - requests==2.32.3
      - six==1.10.0
      - tenacity==5.0.4
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/DukeDSClient

ddsc/core/tests/test_projectuploader.py::TestCreateSmallFileCommand::test_after_run_when_file_is_sent ddsc/core/tests/test_util.py::TestProgressBar::test_show_no_waiting ddsc/core/tests/test_util.py::TestProgressBar::test_show_with_waiting ddsc/core/tests/test_util.py::TestProgressPrinter::test_general_functionality ddsc/core/tests/test_util.py::TestProgressPrinter::test_transferring_item_override_msg_verb
ddsc/core/tests/test_projectuploader.py::TestUploadContext::test_can_pickle ddsc/core/tests/test_projectuploader.py::TestUploadContext::test_done_waiting ddsc/core/tests/test_projectuploader.py::TestUploadContext::test_start_waiting ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_get_report_changes_exist ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_get_report_no_changes ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_nested_directories ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_nested_directories_skip_parents ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_single_empty_existing_directory ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_single_empty_non_existant_directory ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_some_files ddsc/core/tests/test_projectuploader.py::TestCreateProjectCommand::test_constructor_fails_for_id ddsc/core/tests/test_projectuploader.py::TestCreateProjectCommand::test_constructor_ok_for_name ddsc/core/tests/test_projectuploader.py::TestCreateProjectCommand::test_upload_project_run ddsc/core/tests/test_projectuploader.py::TestCreateSmallFile::test_create_small_file_hash_matches ddsc/core/tests/test_projectuploader.py::TestCreateSmallFile::test_create_small_file_passes_zero_index ddsc/core/tests/test_projectuploader.py::TestProjectUploader::test_process_large_file_sets_file_hash_and_alg ddsc/core/tests/test_projectuploader.py::TestProjectUploader::test_run_sorts_new_files_first ddsc/core/tests/test_projectuploader.py::TestProjectUploader::test_run_with_large_files_hash_matching ddsc/core/tests/test_projectuploader.py::TestProjectUploader::test_upload_large_files__updates_watcher ddsc/core/tests/test_projectuploader.py::TestHashFileCommand::test_before_run_shows_checking_message ddsc/core/tests/test_projectuploader.py::TestHashFileCommand::test_function_hashes_file ddsc/core/tests/test_projectuploader.py::TestCreateSmallFileCommand::test_after_run_when_file_is_already_good ddsc/core/tests/test_projectuploader.py::TestCreateSmallFileCommand::test_before_run_updates_watcher ddsc/core/tests/test_util.py::TestUtil::test_verify_terminal_encoding_ascii_raises ddsc/core/tests/test_util.py::TestUtil::test_verify_terminal_encoding_empty_is_ok ddsc/core/tests/test_util.py::TestUtil::test_verify_terminal_encoding_lower ddsc/core/tests/test_util.py::TestUtil::test_verify_terminal_encoding_none_is_ok ddsc/core/tests/test_util.py::TestUtil::test_verify_terminal_encoding_upper ddsc/core/tests/test_util.py::TestProgressBar::test_show_running ddsc/core/tests/test_util.py::TestProgressBar::test_show_waiting ddsc/core/tests/test_util.py::TestProgressPrinter::test_done_waiting ddsc/core/tests/test_util.py::TestProgressPrinter::test_increment_progress ddsc/core/tests/test_util.py::TestProgressPrinter::test_start_waiting_debounces ddsc/core/tests/test_util.py::TestRemotePath::test_add_leading_slash ddsc/core/tests/test_util.py::TestRemotePath::test_split ddsc/core/tests/test_util.py::TestRemotePath::test_strip_leading_slash ddsc/core/tests/test_util.py::TestHumanizeBytes::test_humanize_bytes ddsc/core/tests/test_util.py::TestPluralFmt::test_plural_fmt ddsc/core/tests/test_util.py::TestJoinWithCommasAndAnd::test_join_with_commas_and_and
ddsc/core/projectuploader.py ddsc/core/download.py ddsc/core/fileuploader.py ddsc/core/util.py
ddsc/core/projectuploader.py ddsc/core/download.py ddsc/core/fileuploader.py ddsc/core/util.py