diff --git a/.travis.yml b/.travis.yml
index c5b41fe..7617ace 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,4 @@
-sudo: false
+dist: trusty  # For python 2.6
 language: python
 matrix:
     include:
diff --git a/docs/index.rst b/docs/index.rst
index 61cdead..7d7ed8c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -106,6 +106,8 @@ Changelog
 
 - **UNRELEASED**
 
+  - Support passing file-like objects (supporting `.read(n)`) as the `content`
+    parameter for Resources.
   - (Unreleased features should be listed here.)
 
 - **1.0.0** (2019-01-09)
diff --git a/mohawk/sender.py b/mohawk/sender.py
index 3fbd1bb..5c599e3 100644
--- a/mohawk/sender.py
+++ b/mohawk/sender.py
@@ -26,8 +26,8 @@ class Sender(HawkAuthority):
     :param method: Method of the request. E.G. POST, GET
     :type method: str
 
-    :param content=EmptyValue: Byte string of request body.
-    :type content=EmptyValue: str
+    :param content=EmptyValue: Byte string of request body or a file-like object.
+    :type content=EmptyValue: str or file-like object
 
     :param content_type=EmptyValue: content-type header value for request.
     :type content_type=EmptyValue: str
diff --git a/mohawk/util.py b/mohawk/util.py
index bb8a14e..0c51428 100644
--- a/mohawk/util.py
+++ b/mohawk/util.py
@@ -46,7 +46,7 @@ def random_string(length):
     return urlsafe_b64encode(os.urandom(length))[:length]
 
 
-def calculate_payload_hash(payload, algorithm, content_type):
+def calculate_payload_hash(payload, algorithm, content_type, block_size=1024):
     """Calculates a hash for a given payload."""
     p_hash = hashlib.new(algorithm)
 
@@ -58,9 +58,18 @@ def calculate_payload_hash(payload, algorithm, content_type):
 
     for i, p in enumerate(parts):
         # Make sure we are about to hash binary strings.
-        if not isinstance(p, six.binary_type):
+        if hasattr(p, "read"):
+            log.debug("part %i being handled as a file object", i)
+            while True:
+                block = p.read(block_size)
+                if not block:
+                    break
+                p_hash.update(block)
+        elif not isinstance(p, six.binary_type):
             p = p.encode('utf8')
-        p_hash.update(p)
+            p_hash.update(p)
+        else:
+            p_hash.update(p)
         parts[i] = p
 
     log.debug('calculating payload hash from:\n{parts}'

Support file objects as content
We have some large files we need to send via an HTTP POST, and the API requires Hawk authentication. Currently this means the entire file has to be loaded into memory so that it can be passed as `content` to `mohawk.Sender`

Can we instead pass an open file object, and then mohawk can read in smaller blocks of data from the file to create the payload hash?
diff --git a/mohawk/tests.py b/mohawk/tests.py
index a2710c6..e680cf8 100644
--- a/mohawk/tests.py
+++ b/mohawk/tests.py
@@ -22,6 +22,7 @@ from .exc import (AlreadyProcessed,
                   MissingContent)
 from .util import (parse_authorization_header,
                    utc_now,
+                   calculate_payload_hash,
                    calculate_ts_mac,
                    validate_credentials)
 from .bewit import (get_bewit,
@@ -443,6 +444,25 @@ class TestSender(Base):
         header = sn.request_header.replace(dlg, 'TAMPERED-WITH')
         self.receive(header)
 
+    def test_file_content(self):
+        method = "POST"
+        content = six.BytesIO(b"FILE CONTENT")
+        sn = self.Sender(method, content=content)
+        self.receive(sn.request_header, method=method, content=content.getvalue())
+
+    def test_binary_file_content(self):
+        method = "POST"
+        content = six.BytesIO(b"\x00\xffCONTENT\xff\x00")
+        sn = self.Sender(method, content=content)
+        self.receive(sn.request_header, method=method, content=content.getvalue())
+
+    @raises(MisComputedContentHash)
+    def test_bad_file_content(self):
+        method = "POST"
+        content = six.BytesIO(b"FILE CONTENT")
+        sn = self.Sender(method, content=content)
+        self.receive(sn.request_header, method=method, content="BAD FILE CONTENT")
+
 
 class TestReceiver(Base):
 
@@ -999,3 +1019,12 @@ class TestBewit(Base):
             'other_id': self.credentials,
         })
         check_bewit(url, credential_lookup=credential_lookup, now=1356420407 + 10)
+
+
+class TestPayloadHash(Base):
+    def test_hash_file_read_blocks(self):
+        payload = six.BytesIO(b"\x00\xffhello world\xff\x00")
+        h1 = calculate_payload_hash(payload, 'sha256', 'application/json', block_size=1)
+        payload.seek(0)
+        h2 = calculate_payload_hash(payload, 'sha256', 'application/json', block_size=1024)
+        self.assertEqual(h1, h2)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.7
requirements/dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
args==0.1.0
Babel==2.14.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
clint==0.5.1
docutils==0.12
exceptiongroup==1.2.2
idna==3.10
imagesize==1.4.1
importlib-metadata==6.7.0
iniconfig==2.0.0
Jinja2==3.1.6
MarkupSafe==2.1.5
mock==5.2.0
-e git+https://github.com/kumar303/mohawk.git@037be67ccf50ae704705e67add44e02737a65d21#egg=mohawk
nose==1.3.7
packaging==24.0
pkginfo==1.10.0
pluggy==1.2.0
Pygments==2.17.2
pytest==7.4.4
pytz==2025.2
requests==2.31.0
requests-toolbelt==1.0.0
six==1.17.0
snowballstemmer==2.2.0
Sphinx==1.4.9
sphinx-rtd-theme==0.5.2
tomli==2.0.1
twine==1.7.4
typing_extensions==4.7.1
urllib3==2.0.7
zipp==3.15.0

name: mohawk
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
  - certifi=2022.12.7=py37h06a4308_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=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - args==0.1.0
      - babel==2.14.0
      - charset-normalizer==3.4.1
      - clint==0.5.1
      - docutils==0.12
      - exceptiongroup==1.2.2
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - jinja2==3.1.6
      - markupsafe==2.1.5
      - mock==5.2.0
      - nose==1.3.7
      - packaging==24.0
      - pkginfo==1.10.0
      - pluggy==1.2.0
      - pygments==2.17.2
      - pytest==7.4.4
      - pytz==2025.2
      - requests==2.31.0
      - requests-toolbelt==1.0.0
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==1.4.9
      - sphinx-rtd-theme==0.5.2
      - tomli==2.0.1
      - twine==1.7.4
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - wheel==0.29.0
      - zipp==3.15.0
prefix: /opt/conda/envs/mohawk

mohawk/tests.py::TestSender::test_bad_file_content mohawk/tests.py::TestSender::test_binary_file_content mohawk/tests.py::TestSender::test_file_content mohawk/tests.py::TestPayloadHash::test_hash_file_read_blocks
mohawk/tests.py::TestConfig::test_no_algo mohawk/tests.py::TestConfig::test_no_credentials mohawk/tests.py::TestConfig::test_no_id mohawk/tests.py::TestConfig::test_no_key mohawk/tests.py::TestConfig::test_non_dict_credentials mohawk/tests.py::TestSender::test_app_ok mohawk/tests.py::TestSender::test_bad_ext mohawk/tests.py::TestSender::test_bad_secret mohawk/tests.py::TestSender::test_cannot_skip_content_only mohawk/tests.py::TestSender::test_cannot_skip_content_type_only mohawk/tests.py::TestSender::test_dlg_ok mohawk/tests.py::TestSender::test_duplicate_keys mohawk/tests.py::TestSender::test_empty_payload_hashing mohawk/tests.py::TestSender::test_empty_payload_hashing_accept_untrusted mohawk/tests.py::TestSender::test_empty_payload_hashing_always_hash_false mohawk/tests.py::TestSender::test_expired_exception_reports_localtime mohawk/tests.py::TestSender::test_expired_ts mohawk/tests.py::TestSender::test_ext_with_all_valid_characters mohawk/tests.py::TestSender::test_ext_with_equality_sign mohawk/tests.py::TestSender::test_ext_with_illegal_chars mohawk/tests.py::TestSender::test_ext_with_illegal_unicode mohawk/tests.py::TestSender::test_ext_with_illegal_utf8 mohawk/tests.py::TestSender::test_ext_with_new_line mohawk/tests.py::TestSender::test_ext_with_quotes mohawk/tests.py::TestSender::test_get_ok mohawk/tests.py::TestSender::test_hash_tampering mohawk/tests.py::TestSender::test_invalid_credentials mohawk/tests.py::TestSender::test_invalid_key mohawk/tests.py::TestSender::test_localtime_offset mohawk/tests.py::TestSender::test_localtime_skew mohawk/tests.py::TestSender::test_missing_payload_details mohawk/tests.py::TestSender::test_non_ascii_content mohawk/tests.py::TestSender::test_non_hawk_scheme mohawk/tests.py::TestSender::test_nonce_fail mohawk/tests.py::TestSender::test_nonce_ok mohawk/tests.py::TestSender::test_post_content_ok mohawk/tests.py::TestSender::test_post_content_type_ok mohawk/tests.py::TestSender::test_post_content_type_with_trailing_charset mohawk/tests.py::TestSender::test_post_ok mohawk/tests.py::TestSender::test_skip_payload_hashing mohawk/tests.py::TestSender::test_tamper_with_content mohawk/tests.py::TestSender::test_tamper_with_content_type mohawk/tests.py::TestSender::test_tamper_with_host mohawk/tests.py::TestSender::test_tamper_with_method mohawk/tests.py::TestSender::test_tamper_with_path mohawk/tests.py::TestSender::test_tamper_with_port mohawk/tests.py::TestSender::test_tamper_with_query mohawk/tests.py::TestSender::test_tamper_with_scheme mohawk/tests.py::TestSender::test_tampered_app mohawk/tests.py::TestSender::test_tampered_dlg mohawk/tests.py::TestSender::test_too_long_header mohawk/tests.py::TestSender::test_unexpected_algorithm mohawk/tests.py::TestSender::test_unknown_id mohawk/tests.py::TestSender::test_unparseable_header mohawk/tests.py::TestReceiver::test_cannot_receive_empty_content_only mohawk/tests.py::TestReceiver::test_cannot_receive_empty_content_type_only mohawk/tests.py::TestReceiver::test_empty_content_with_content_type mohawk/tests.py::TestReceiver::test_expected_unhashed_content_with_empty_content_type mohawk/tests.py::TestReceiver::test_expected_unhashed_content_with_no_content_type mohawk/tests.py::TestReceiver::test_expected_unhashed_empty_content mohawk/tests.py::TestReceiver::test_expected_unhashed_empty_content_with_content_type mohawk/tests.py::TestReceiver::test_expected_unhashed_no_content mohawk/tests.py::TestReceiver::test_expected_unhashed_no_content_with_content_type mohawk/tests.py::TestReceiver::test_get_ok mohawk/tests.py::TestReceiver::test_invalid_credentials_lookup mohawk/tests.py::TestReceiver::test_missing_authorization mohawk/tests.py::TestReceiver::test_no_content_with_content_type mohawk/tests.py::TestReceiver::test_post_ok mohawk/tests.py::TestReceiver::test_receive_wrong_content mohawk/tests.py::TestReceiver::test_receive_wrong_content_type mohawk/tests.py::TestReceiver::test_receive_wrong_method mohawk/tests.py::TestReceiver::test_receive_wrong_url mohawk/tests.py::TestReceiver::test_respond_with_bad_ts_skew_ok mohawk/tests.py::TestReceiver::test_respond_with_expired_ts mohawk/tests.py::TestReceiver::test_respond_with_ext mohawk/tests.py::TestReceiver::test_respond_with_unhashed_content mohawk/tests.py::TestReceiver::test_respond_with_wrong_app mohawk/tests.py::TestReceiver::test_respond_with_wrong_content mohawk/tests.py::TestReceiver::test_respond_with_wrong_content_type mohawk/tests.py::TestReceiver::test_respond_with_wrong_dlg mohawk/tests.py::TestReceiver::test_respond_with_wrong_method mohawk/tests.py::TestReceiver::test_respond_with_wrong_nonce mohawk/tests.py::TestReceiver::test_respond_with_wrong_url mohawk/tests.py::TestSendAndReceive::test mohawk/tests.py::TestBewit::test_bewit mohawk/tests.py::TestBewit::test_bewit_invalid_method mohawk/tests.py::TestBewit::test_bewit_with_backslashes_in_id mohawk/tests.py::TestBewit::test_bewit_with_binary_id mohawk/tests.py::TestBewit::test_bewit_with_ext mohawk/tests.py::TestBewit::test_bewit_with_invalid_ext mohawk/tests.py::TestBewit::test_bewit_with_nonce mohawk/tests.py::TestBewit::test_bewit_with_port mohawk/tests.py::TestBewit::test_parse_bewit mohawk/tests.py::TestBewit::test_parse_bewit_with_ext mohawk/tests.py::TestBewit::test_parse_bewit_with_ext_and_backslashes mohawk/tests.py::TestBewit::test_parse_invalid_bewit_with_only_one_part mohawk/tests.py::TestBewit::test_parse_invalid_bewit_with_only_two_parts mohawk/tests.py::TestBewit::test_strip_bewit mohawk/tests.py::TestBewit::test_strip_url_without_bewit mohawk/tests.py::TestBewit::test_validate_bewit mohawk/tests.py::TestBewit::test_validate_bewit_with_ext mohawk/tests.py::TestBewit::test_validate_bewit_with_ext_and_backslashes mohawk/tests.py::TestBewit::test_validate_bewit_with_unknown_credentials mohawk/tests.py::TestBewit::test_validate_expired_bewit
mohawk/sender.py .travis.yml mohawk/util.py docs/index.rst
mohawk/sender.py .travis.yml mohawk/util.py docs/index.rst