Hello community,

here is the log from the commit of package python-mohawk for openSUSE:Factory 
checked in at 2019-01-11 14:06:07
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-mohawk (Old)
 and      /work/SRC/openSUSE:Factory/.python-mohawk.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-mohawk"

Fri Jan 11 14:06:07 2019 rev:3 rq:664544 version:1.0.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-mohawk/python-mohawk.changes      
2018-12-24 11:39:48.501536420 +0100
+++ /work/SRC/openSUSE:Factory/.python-mohawk.new.28833/python-mohawk.changes   
2019-01-11 14:06:58.127710717 +0100
@@ -1,0 +2,24 @@
+Fri Jan 11 06:41:11 UTC 2019 - [email protected]
+
+- Update to version 1.0.0:
+  * Security related: Bewit MACs were not compared in constant time
+    and were thus possibly circumventable by an attacker.
+  * Breaking change: Escape characters in header values (such as a
+    back slash) are no longer allowed, potentially breaking clients
+    that depended on this behavior.
+  * A sender is allowed to omit the content hash as long as their
+    request has no content. The `mohawk.Receiver` will skip the
+    content hash check in this situation, regardless of the value
+    of accept_untrusted_content.
+  * Introduced max limit of 4096 characters in the Authorization
+    header.
+  * Changed default values of content and content_type arguments to
+    `mohawk.base.EmptyValue` in order to differentiate between
+    misconfiguration and cases where these arguments are explicitly
+    given as None (as with some web frameworks).
+  * Failing to pass content and content_type arguments to
+    `mohawk.Receiver` or `mohawk.Sender.accept_response` without
+    specifying accept_untrusted_content=True will now raise
+    `mohawk.exc.MissingContent` instead of `ValueError`.
+
+-------------------------------------------------------------------

Old:
----
  mohawk-0.3.4.tar.gz

New:
----
  mohawk-1.0.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-mohawk.spec ++++++
--- /var/tmp/diff_new_pack.Eq5CpO/_old  2019-01-11 14:06:58.583710259 +0100
+++ /var/tmp/diff_new_pack.Eq5CpO/_new  2019-01-11 14:06:58.583710259 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-mohawk
 #
-# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
 # Copyright (c) 2017 The openSUSE Project.
 #
 # All modifications and additions to the file contributed by third parties
@@ -20,7 +20,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %bcond_without test
 Name:           python-mohawk
-Version:        0.3.4
+Version:        1.0.0
 Release:        0
 Summary:        Library for Hawk HTTP authorization
 License:        MPL-2.0

++++++ mohawk-0.3.4.tar.gz -> mohawk-1.0.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/PKG-INFO new/mohawk-1.0.0/PKG-INFO
--- old/mohawk-0.3.4/PKG-INFO   2017-01-07 21:39:21.000000000 +0100
+++ new/mohawk-1.0.0/PKG-INFO   2019-01-09 23:01:05.000000000 +0100
@@ -1,12 +1,23 @@
 Metadata-Version: 1.1
 Name: mohawk
-Version: 0.3.4
+Version: 1.0.0
 Summary: Library for Hawk HTTP authorization
 Home-page: https://github.com/kumar303/mohawk
 Author: Kumar McMillan, Austin King
 Author-email: [email protected]
 License: MPL 2.0 (Mozilla Public License)
-Description: UNKNOWN
+Description: 
+        Hawk lets two parties securely communicate with each other using
+        messages signed by a shared key.
+        It is based on HTTP MAC access authentication (which
+        was based on parts of OAuth 1.0).
+        
+        The Mohawk API is a little different from that of the Node library
+        (i.e. https://github.com/hueniverse/hawk).
+        It was redesigned to be more intuitive to developers, less prone to 
security problems, and more Pythonic.
+        
+        Read more: https://github.com/kumar303/mohawk/
+              
 Platform: UNKNOWN
 Classifier: Intended Audience :: Developers
 Classifier: Natural Language :: English
@@ -15,5 +26,8 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 2.6
 Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
 Classifier: Topic :: Internet :: WWW/HTTP
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/README.rst new/mohawk-1.0.0/README.rst
--- old/mohawk-0.3.4/README.rst 2016-07-12 22:25:50.000000000 +0200
+++ new/mohawk-1.0.0/README.rst 2019-01-09 06:30:25.000000000 +0100
@@ -20,6 +20,17 @@
 Mohawk is an alternate Python implementation of the
 `Hawk HTTP authorization scheme`_.
 
+Hawk lets two parties securely communicate with each other using
+messages signed by a shared key.
+It is based on `HTTP MAC access authentication`_ (which
+was based on parts of `OAuth 1.0`_).
+
+The Mohawk API is a little different from that of the Node library
+(i.e. `the living Hawk spec <https://github.com/hueniverse/hawk>`_).
+It was redesigned to be more intuitive to developers, less prone to security 
problems, and more Pythonic.
+
 Full documentation: https://mohawk.readthedocs.io/
 
 .. _`Hawk HTTP authorization scheme`: https://github.com/hueniverse/hawk
+.. _`HTTP MAC access authentication`: 
http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+.. _`OAuth 1.0`: http://tools.ietf.org/html/rfc5849
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/base.py 
new/mohawk-1.0.0/mohawk/base.py
--- old/mohawk-0.3.4/mohawk/base.py     2016-03-09 18:15:46.000000000 +0100
+++ new/mohawk-1.0.0/mohawk/base.py     2019-01-09 22:03:48.000000000 +0100
@@ -8,7 +8,8 @@
 from .exc import (AlreadyProcessed,
                   MacMismatch,
                   MisComputedContentHash,
-                  TokenExpired)
+                  TokenExpired,
+                  MissingContent)
 from .util import (calculate_mac,
                    calculate_payload_hash,
                    calculate_ts_mac,
@@ -21,6 +22,26 @@
 log = logging.getLogger(__name__)
 
 
+class HawkEmptyValue(object):
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+    def __ne__(self, other):
+        return (not self.__eq__(other))
+
+    def __nonzero__(self):
+        return False
+
+    def __bool__(self):
+        return False
+
+    def __repr__(self):
+        return 'EmptyValue'
+
+EmptyValue = HawkEmptyValue()
+
+
 class HawkAuthority:
 
     def _authorize(self, mac_type, parsed_header, resource,
@@ -39,20 +60,30 @@
                               'theirs: {theirs}'
                               .format(ours=mac, theirs=their_mac))
 
-        if 'hash' not in parsed_header and accept_untrusted_content:
-            # The request did not hash its content.
-            log.debug('NOT calculating/verifiying payload hash '
-                      '(no hash in header)')
-            check_hash = False
-            content_hash = None
-        else:
-            check_hash = True
-            content_hash = resource.gen_content_hash()
+        check_hash = True
 
-        if check_hash and not their_hash:
-            log.info('request unexpectedly did not hash its content')
+        if 'hash' not in parsed_header:
+            # The request did not hash its content.
+            if not resource.content and not resource.content_type:
+                # It is acceptable to not receive a hash if there is no content
+                # to hash.
+                log.debug('NOT calculating/verifying payload hash '
+                          '(no hash in header, request body is empty)')
+                check_hash = False
+            elif accept_untrusted_content:
+                # Allow the request, even if it has content. Missing content or
+                # content_type values will be coerced to the empty string for
+                # hashing purposes.
+                log.debug('NOT calculating/verifying payload hash '
+                          '(no hash in header, accept_untrusted_content=True)')
+                check_hash = False
 
         if check_hash:
+            if not their_hash:
+                log.info('request unexpectedly did not hash its content')
+
+            content_hash = resource.gen_content_hash()
+
             if not strings_match(content_hash, their_hash):
                 # The hash declared in the header is incorrect.
                 # Content could have been tampered with.
@@ -77,8 +108,8 @@
                                                ts=parsed_header['ts'],
                                                id=resource.credentials['id']))
         else:
-            log.warn('seen_nonce was None; not checking nonce. '
-                     'You may be vulnerable to replay attacks')
+            log.warning('seen_nonce was None; not checking nonce. '
+                        'You may be vulnerable to replay attacks')
 
         their_ts = int(their_timestamp or parsed_header['ts'])
 
@@ -147,14 +178,67 @@
 
 class Resource:
     """
-    Normalized request/response resource.
+    Normalized request / response resource.
+
+    :param credentials:
+        A dict of credentials; it must have the keys:
+        ``id``, ``key``, and ``algorithm``.
+        See :ref:`sending-request` for an example.
+    :type credentials_map: dict
+
+    :param url: Absolute URL of the request / response.
+    :type url: str
+
+    :param method: Method of the request / response. E.G. POST, GET
+    :type method: str
+
+    :param content=EmptyValue: Byte string of request / response body.
+    :type content=EmptyValue: str
+
+    :param content_type=EmptyValue: content-type header value for request / 
response.
+    :type content_type=EmptyValue: str
+
+    :param always_hash_content=True:
+        When True, ``content`` and ``content_type`` must be provided.
+        Read :ref:`skipping-content-checks` to learn more.
+    :type always_hash_content=True: bool
+
+    :param ext=None:
+        An external `Hawk`_ string. If not None, this value will be
+        signed so that the sender can trust it.
+    :type ext=None: str
+
+    :param app=None:
+        A `Hawk`_ string identifying an external application.
+    :type app=None: str
+
+    :param dlg=None:
+        A `Hawk`_ string identifying a "delegated by" value.
+    :type dlg=None: str
+
+    :param timestamp=utc_now():
+        A unix timestamp integer, in UTC
+    :type timestamp: int
+
+    :param nonce=None:
+        A string that when coupled with the timestamp will
+        uniquely identify this request / response.
+    :type nonce=None: str
+
+    :param seen_nonce=None:
+        A callable that returns True if a nonce has been seen.
+        See :ref:`nonce` for details.
+    :type seen_nonce=None: callable
+
+    .. _`Hawk`: https://github.com/hueniverse/hawk
     """
 
     def __init__(self, **kw):
         self.credentials = kw.pop('credentials')
+        self.credentials['id'] = prepare_header_val(self.credentials['id'])
         self.method = kw.pop('method').upper()
-        self.content = kw.pop('content', None)
-        self.content_type = kw.pop('content_type', None)
+        self.content = kw.pop('content', EmptyValue)
+        self.content_type = kw.pop('content_type', EmptyValue)
         self.always_hash_content = kw.pop('always_hash_content', True)
         self.ext = kw.pop('ext', None)
         self.app = kw.pop('app', None)
@@ -192,14 +276,14 @@
         return self._content_hash
 
     def gen_content_hash(self):
-        if self.content is None or self.content_type is None:
+        if self.content == EmptyValue or self.content_type == EmptyValue:
             if self.always_hash_content:
                 # Be really strict about allowing developers to skip content
                 # hashing. If they get this far they may be unintentiionally
                 # skipping it.
-                raise ValueError(
+                raise MissingContent(
                     'payload content and/or content_type cannot be '
-                    'empty without an explicit allowance')
+                    'empty when always_hash_content is True')
             log.debug('NOT hashing content')
             self._content_hash = None
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/bewit.py 
new/mohawk-1.0.0/mohawk/bewit.py
--- old/mohawk-0.3.4/mohawk/bewit.py    2016-02-25 06:18:56.000000000 +0100
+++ new/mohawk-1.0.0/mohawk/bewit.py    2019-01-09 21:26:13.000000000 +0100
@@ -7,7 +7,9 @@
 
 from .base import Resource
 from .util import (calculate_mac,
-                   utc_now)
+                   strings_match,
+                   utc_now,
+                   validate_header_attr)
 from .exc import (CredentialsLookupError,
                   InvalidBewit,
                   MacMismatch,
@@ -40,25 +42,15 @@
     if resource.ext is None:
         ext = ''
     else:
+        validate_header_attr(resource.ext, name='ext')
         ext = resource.ext
 
-    # Strip out \ from the client id
-    # since that can break parsing the response
-    # NB that the canonical implementation does not do this as of
-    # Oct 28, 2015, so this could break compat.
-    # We can leave \ in ext since validators can limit how many \ they split
-    # on (although again, the canonical implementation does not do this)
-    client_id = six.text_type(resource.credentials['id'])
-    if "\\" in client_id:
-        log.warn("Stripping backslash character(s) '\\' from client_id")
-        client_id = client_id.replace("\\", "")
-
     # b64encode works only with bytes in python3, but all of our parameters are
     # in unicode, so we need to encode them. The cleanest way to do this that
     # works in both python 2 and 3 is to use string formatting to get a
     # unicode string, and then explicitly encode it to bytes.
     inner_bewit = u"{id}\\{exp}\\{mac}\\{ext}".format(
-        id=client_id,
+        id=resource.credentials['id'],
         exp=resource.timestamp,
         mac=mac,
         ext=ext,
@@ -83,10 +75,10 @@
     :type bewit: str
     """
     decoded_bewit = b64decode(bewit).decode('ascii')
-    bewit_parts = decoded_bewit.split("\\", 3)
+    bewit_parts = decoded_bewit.split("\\")
     if len(bewit_parts) != 4:
         raise InvalidBewit('Expected 4 parts to bewit: %s' % decoded_bewit)
-    return bewittuple(*decoded_bewit.split("\\", 3))
+    return bewittuple(*bewit_parts)
 
 
 def strip_bewit(url):
@@ -146,7 +138,7 @@
     mac = calculate_mac('bewit', res, None)
     mac = mac.decode('ascii')
 
-    if mac != bewit.mac:
+    if not strings_match(mac, bewit.mac):
         raise MacMismatch('bewit with mac {bewit_mac} did not match expected 
mac {expected_mac}'
                           .format(bewit_mac=bewit.mac,
                                   expected_mac=mac))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/exc.py 
new/mohawk-1.0.0/mohawk/exc.py
--- old/mohawk-0.3.4/mohawk/exc.py      2017-01-07 21:33:03.000000000 +0100
+++ new/mohawk-1.0.0/mohawk/exc.py      2018-11-02 22:44:22.000000000 +0100
@@ -1,6 +1,11 @@
 """
 If you want to catch any exception that might be raised,
 catch :class:`mohawk.exc.HawkFail`.
+
+.. important::
+
+    Never expose an exception message publicly, say, in an HTTP
+    response, as it may provide hints to an attacker.
 """
 
 
@@ -96,3 +101,11 @@
     The bewit is invalid; e.g. it doesn't contain the right number of
     parameters.
     """
+
+
+class MissingContent(HawkFail):
+    """
+    A payload's `content` or `content_type` were not provided.
+
+    See :ref:`skipping-content-checks` for details.
+    """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/receiver.py 
new/mohawk-1.0.0/mohawk/receiver.py
--- old/mohawk-0.3.4/mohawk/receiver.py 2017-01-07 21:33:03.000000000 +0100
+++ new/mohawk-1.0.0/mohawk/receiver.py 2018-11-02 22:44:22.000000000 +0100
@@ -1,7 +1,10 @@
 import logging
 import sys
 
-from .base import default_ts_skew_in_seconds, HawkAuthority, Resource
+from .base import (default_ts_skew_in_seconds,
+                   HawkAuthority,
+                   Resource,
+                   EmptyValue)
 from .exc import CredentialsLookupError, MissingAuthorization
 from .util import (calculate_mac,
                    parse_authorization_header,
@@ -33,17 +36,15 @@
     :param method: Method of the request. E.G. POST, GET
     :type method: str
 
-    :param content=None: Byte string of request body.
-    :type content=None: str
+    :param content=EmptyValue: Byte string of request body.
+    :type content=EmptyValue: str
 
-    :param content_type=None: content-type header value for request.
-    :type content_type=None: str
+    :param content_type=EmptyValue: content-type header value for request.
+    :type content_type=EmptyValue: str
 
     :param accept_untrusted_content=False:
-        When True, allow requests that do not hash their content or
-        allow None type ``content`` and ``content_type``
-        arguments. Read :ref:`skipping-content-checks`
-        to learn more.
+        When True, allow requests that do not hash their content.
+        Read :ref:`skipping-content-checks` to learn more.
     :type accept_untrusted_content=False: bool
 
     :param localtime_offset_in_seconds=0:
@@ -65,8 +66,8 @@
                  request_header,
                  url,
                  method,
-                 content=None,
-                 content_type=None,
+                 content=EmptyValue,
+                 content_type=EmptyValue,
                  seen_nonce=None,
                  localtime_offset_in_seconds=0,
                  accept_untrusted_content=False,
@@ -120,8 +121,8 @@
         self.resource = resource
 
     def respond(self,
-                content=None,
-                content_type=None,
+                content=EmptyValue,
+                content_type=EmptyValue,
                 always_hash_content=True,
                 ext=None):
         """
@@ -130,14 +131,14 @@
         This generates the :attr:`mohawk.Receiver.response_header`
         attribute.
 
-        :param content=None: Byte string of response body that will be sent.
-        :type content=None: str
+        :param content=EmptyValue: Byte string of response body that will be 
sent.
+        :type content=EmptyValue: str
 
-        :param content_type=None: content-type header value for response.
-        :type content_type=None: str
+        :param content_type=EmptyValue: content-type header value for response.
+        :type content_type=EmptyValue: str
 
         :param always_hash_content=True:
-            When True, ``content`` and ``content_type`` cannot be None.
+            When True, ``content`` and ``content_type`` must be provided.
             Read :ref:`skipping-content-checks` to learn more.
         :type always_hash_content=True: bool
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/sender.py 
new/mohawk-1.0.0/mohawk/sender.py
--- old/mohawk-0.3.4/mohawk/sender.py   2015-06-21 20:42:01.000000000 +0200
+++ new/mohawk-1.0.0/mohawk/sender.py   2018-11-02 22:44:22.000000000 +0100
@@ -1,6 +1,9 @@
 import logging
 
-from .base import default_ts_skew_in_seconds, HawkAuthority, Resource
+from .base import (default_ts_skew_in_seconds,
+                   HawkAuthority,
+                   Resource,
+                   EmptyValue)
 from .util import (calculate_mac,
                    parse_authorization_header,
                    validate_credentials)
@@ -23,14 +26,14 @@
     :param method: Method of the request. E.G. POST, GET
     :type method: str
 
-    :param content=None: Byte string of request body.
-    :type content=None: str
+    :param content=EmptyValue: Byte string of request body.
+    :type content=EmptyValue: str
 
-    :param content_type=None: content-type header value for request.
-    :type content_type=None: str
+    :param content_type=EmptyValue: content-type header value for request.
+    :type content_type=EmptyValue: str
 
     :param always_hash_content=True:
-        When True, ``content`` and ``content_type`` cannot be None.
+        When True, ``content`` and ``content_type`` must be provided.
         Read :ref:`skipping-content-checks` to learn more.
     :type always_hash_content=True: bool
 
@@ -68,8 +71,8 @@
     def __init__(self, credentials,
                  url,
                  method,
-                 content=None,
-                 content_type=None,
+                 content=EmptyValue,
+                 content_type=EmptyValue,
                  always_hash_content=True,
                  nonce=None,
                  ext=None,
@@ -102,8 +105,8 @@
 
     def accept_response(self,
                         response_header,
-                        content=None,
-                        content_type=None,
+                        content=EmptyValue,
+                        content_type=EmptyValue,
                         accept_untrusted_content=False,
                         localtime_offset_in_seconds=0,
                         timestamp_skew_in_seconds=default_ts_skew_in_seconds,
@@ -116,18 +119,16 @@
             such as one created by :class:`mohawk.Receiver`.
         :type response_header: str
 
-        :param content=None: Byte string of the response body received.
-        :type content=None: str
+        :param content=EmptyValue: Byte string of the response body received.
+        :type content=EmptyValue: str
 
-        :param content_type=None:
+        :param content_type=EmptyValue:
             Content-Type header value of the response received.
-        :type content_type=None: str
+        :type content_type=EmptyValue: str
 
         :param accept_untrusted_content=False:
-            When True, allow responses that do not hash their content or
-            allow None type ``content`` and ``content_type``
-            arguments. Read :ref:`skipping-content-checks`
-            to learn more.
+            When True, allow responses that do not hash their content.
+            Read :ref:`skipping-content-checks` to learn more.
         :type accept_untrusted_content=False: bool
 
         :param localtime_offset_in_seconds=0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/tests.py 
new/mohawk-1.0.0/mohawk/tests.py
--- old/mohawk-0.3.4/mohawk/tests.py    2017-01-07 21:33:03.000000000 +0100
+++ new/mohawk-1.0.0/mohawk/tests.py    2019-01-09 21:26:13.000000000 +0100
@@ -1,4 +1,5 @@
 import sys
+import warnings
 from unittest import TestCase
 from base64 import b64decode, urlsafe_b64encode
 
@@ -7,16 +8,18 @@
 import six
 
 from . import Receiver, Sender
-from .base import Resource
+from .base import Resource, EmptyValue
 from .exc import (AlreadyProcessed,
                   BadHeaderValue,
                   CredentialsLookupError,
+                  HawkFail,
                   InvalidCredentials,
                   MacMismatch,
                   MisComputedContentHash,
                   MissingAuthorization,
                   TokenExpired,
-                  InvalidBewit)
+                  InvalidBewit,
+                  MissingContent)
 from .util import (parse_authorization_header,
                    utc_now,
                    calculate_ts_mac,
@@ -27,6 +30,10 @@
                     parse_bewit)
 
 
+# Ensure deprecation warnings are turned to exceptions
+warnings.filterwarnings('error')
+
+
 class Base(TestCase):
 
     def setUp(self):
@@ -136,29 +143,64 @@
         self.receive(sn.request_header, method=method, content=content,
                      content_type='application/json; charset=other')
 
-    @raises(ValueError)
+    @raises(MissingContent)
     def test_missing_payload_details(self):
-        self.Sender(method='POST', content=None, content_type=None)
+        self.Sender(method='POST', content=EmptyValue,
+                    content_type=EmptyValue)
 
     def test_skip_payload_hashing(self):
         method = 'POST'
         content = '{"bar": "foobs"}'
         content_type = 'application/json'
-        sn = self.Sender(method=method, content=None, content_type=None,
+        sn = self.Sender(method=method, content=EmptyValue,
+                         content_type=EmptyValue,
                          always_hash_content=False)
+        self.assertFalse('hash="' in sn.request_header)
         self.receive(sn.request_header, method=method, content=content,
                      content_type=content_type,
                      accept_untrusted_content=True)
 
-    @raises(ValueError)
+    def test_empty_payload_hashing(self):
+        method = 'GET'
+        content = None
+        content_type = None
+        sn = self.Sender(method=method, content=content,
+                         content_type=content_type)
+        self.assertTrue('hash="' in sn.request_header)
+        self.receive(sn.request_header, method=method, content=content,
+                     content_type=content_type)
+
+    def test_empty_payload_hashing_always_hash_false(self):
+        method = 'GET'
+        content = None
+        content_type = None
+        sn = self.Sender(method=method, content=content,
+                         content_type=content_type,
+                         always_hash_content=False)
+        self.assertTrue('hash="' in sn.request_header)
+        self.receive(sn.request_header, method=method, content=content,
+                     content_type=content_type)
+
+    def test_empty_payload_hashing_accept_untrusted(self):
+        method = 'GET'
+        content = None
+        content_type = None
+        sn = self.Sender(method=method, content=content,
+                         content_type=content_type)
+        self.assertTrue('hash="' in sn.request_header)
+        self.receive(sn.request_header, method=method, content=content,
+                     content_type=content_type,
+                     accept_untrusted_content=True)
+
+    @raises(MissingContent)
     def test_cannot_skip_content_only(self):
-        self.Sender(method='POST', content=None,
+        self.Sender(method='POST', content=EmptyValue,
                     content_type='application/json')
 
-    @raises(ValueError)
+    @raises(MissingContent)
     def test_cannot_skip_content_type_only(self):
         self.Sender(method='POST', content='{"foo": "bar"}',
-                    content_type=None)
+                    content_type=EmptyValue)
 
     @raises(MacMismatch)
     def test_tamper_with_host(self):
@@ -308,17 +350,21 @@
         header = sn.request_header.replace('my external data', 'TAMPERED')
         self.receive(header)
 
+    @raises(BadHeaderValue)
+    def test_duplicate_keys(self):
+        sn = self.Sender(ext='someext')
+        header = sn.request_header + ', ext="otherext"'
+        self.receive(header)
+
+    @raises(BadHeaderValue)
     def test_ext_with_quotes(self):
         sn = self.Sender(ext='quotes=""')
         self.receive(sn.request_header)
-        parsed = parse_authorization_header(sn.request_header)
-        eq_(parsed['ext'], 'quotes=""')
 
+    @raises(BadHeaderValue)
     def test_ext_with_new_line(self):
         sn = self.Sender(ext="new line \n in the middle")
         self.receive(sn.request_header)
-        parsed = parse_authorization_header(sn.request_header)
-        eq_(parsed['ext'], "new line \n in the middle")
 
     def test_ext_with_equality_sign(self):
         sn = self.Sender(ext="foo=bar&foo2=bar2;foo3=bar3")
@@ -326,19 +372,47 @@
         parsed = parse_authorization_header(sn.request_header)
         eq_(parsed['ext'], "foo=bar&foo2=bar2;foo3=bar3")
 
+    @raises(HawkFail)
+    def test_non_hawk_scheme(self):
+        parse_authorization_header('Basic user:base64pw')
+
+    @raises(HawkFail)
+    def test_invalid_key(self):
+        parse_authorization_header('Hawk mac="validmac" unknownkey="value"')
+
+    def test_ext_with_all_valid_characters(self):
+        valid_characters = "!#$%&'()*+,-./:;<=>?@[]^_`{|}~ azAZ09_"
+        sender = self.Sender(ext=valid_characters)
+        parsed = parse_authorization_header(sender.request_header)
+        eq_(parsed['ext'], valid_characters)
+
     @raises(BadHeaderValue)
     def test_ext_with_illegal_chars(self):
         self.Sender(ext="something like \t is illegal")
 
+    def test_unparseable_header(self):
+        try:
+            parse_authorization_header('Hawk mac="somemac", unparseable')
+        except BadHeaderValue as exc:
+            error_msg = str(exc)
+            self.assertTrue("Couldn't parse Hawk header" in error_msg)
+            self.assertTrue("unparseable" in error_msg)
+        else:
+            self.fail('should raise')
+
     @raises(BadHeaderValue)
     def test_ext_with_illegal_unicode(self):
         self.Sender(ext=u'Ivan Kristi\u0107')
 
     @raises(BadHeaderValue)
+    def test_too_long_header(self):
+        sn = self.Sender(ext='a'*5000)
+        self.receive(sn.request_header)
+
+    @raises(BadHeaderValue)
     def test_ext_with_illegal_utf8(self):
         # This isn't allowed because the escaped byte chars are out of
-        # range. It's a little odd but this is what the Node lib does
-        # implicitly with its regex.
+        # range.
         self.Sender(ext=u'Ivan Kristi\u0107'.encode('utf8'))
 
     def test_app_ok(self):
@@ -560,24 +634,125 @@
         wrong_sender = self.sender
         self.receive(content='TAMPERED WITH', sender=wrong_sender)
 
+    def test_expected_unhashed_empty_content(self):
+        # This test sets up a scenario where the receiver will receive empty
+        # strings for content and content_type and no content hash in the auth
+        # header.
+        # This is to account for callers that might provide empty strings for
+        # the payload when in fact there is literally no content. In this case,
+        # mohawk depends on the presence of the content hash in the auth header
+        # to determine how to treat the empty strings: no hash in the header
+        # implies that no hashing is expected to occur on the server.
+        self.receive(content='',
+                     content_type='',
+                     sender_kw=dict(content=EmptyValue,
+                                    content_type=EmptyValue,
+                                    always_hash_content=False))
+
     @raises(MisComputedContentHash)
-    def test_unexpected_unhashed_content(self):
-        self.receive(sender_kw=dict(content=None, content_type=None,
+    def test_expected_unhashed_empty_content_with_content_type(self):
+        # This test sets up a scenario where the receiver will receive an
+        # empty content string and no content hash in the auth header, but
+        # some value for content_type.
+        # This is to confirm that the hash is calculated and compared (to the
+        # hash of mock empty payload, which should fail) when it appears that
+        # the sender has sent a 0-length payload body.
+        self.receive(content='',
+                     content_type='text/plain',
+                     sender_kw=dict(content=EmptyValue,
+                                    content_type=EmptyValue,
                                     always_hash_content=False))
 
-    @raises(ValueError)
+    @raises(MisComputedContentHash)
+    def test_expected_unhashed_content_with_empty_content_type(self):
+        # This test sets up a scenario where the receiver will receive some
+        # content but the empty string for the content_type and no content hash
+        # in the auth header.
+        # This is to confirm that the hash is calculated and compared (to the
+        # hash of mock empty payload, which should fail) when the sender has
+        # sent unhashed content.
+        self.receive(content='some content',
+                     content_type='',
+                     sender_kw=dict(content=EmptyValue,
+                                    content_type=EmptyValue,
+                                    always_hash_content=False))
+
+    def test_empty_content_with_content_type(self):
+        # This test sets up a scenario where the receiver will receive an
+        # empty content string, some value for content_type and a content hash.
+        # This is to confirm that the hash is calculated and compared correctly
+        # when the sender has sent a hashed 0-length payload body.
+        self.receive(content='',
+                     content_type='text/plain',
+                     sender_kw=dict(content='',
+                                    content_type='text/plain'))
+
+    def test_expected_unhashed_no_content(self):
+        # This test sets up a scenario where the receiver will receive None for
+        # content and content_type and no content hash in the auth header.
+        # This is like test_expected_unhashed_empty_content(), but tests for
+        # the less ambiguous case where the caller has explicitly passed in 
None
+        # to indicate that there is no content to hash.
+        self.receive(content=None,
+                     content_type=None,
+                     sender_kw=dict(content=EmptyValue,
+                                    content_type=EmptyValue,
+                                    always_hash_content=False))
+
+    @raises(MisComputedContentHash)
+    def test_expected_unhashed_no_content_with_content_type(self):
+        # This test sets up a scenario where the receiver will receive None for
+        # content and no content hash in the auth header, but some value for
+        # content_type.
+        # In this case, the content will be coerced to the empty string for
+        # hashing purposes. The request should fail, as the there is no content
+        # hash in the request to compare against. While this may not be in
+        # accordance with the js reference spec, it's the safest (ie. most
+        # secure) way of handling this bizarre set of circumstances.
+        self.receive(content=None,
+                     content_type='text/plain',
+                     sender_kw=dict(content=EmptyValue,
+                                    content_type=EmptyValue,
+                                    always_hash_content=False))
+
+    @raises(MisComputedContentHash)
+    def test_expected_unhashed_content_with_no_content_type(self):
+        # This test sets up a scenario where the receiver will receive some
+        # content but no value for the content_type and no content hash in
+        # the auth header.
+        # This is to confirm that the hash is calculated and compared (to the
+        # hash of mock empty payload, which should fail) when the sender has
+        # sent unhashed content.
+        self.receive(content='some content',
+                     content_type=None,
+                     sender_kw=dict(content=EmptyValue,
+                                    content_type=EmptyValue,
+                                    always_hash_content=False))
+
+    def test_no_content_with_content_type(self):
+        # This test sets up a scenario where the receiver will receive None for
+        # the content string, some value for content_type and a content hash.
+        # This is to confirm that coercing None to the empty string when a hash
+        # is expected allows the hash to be calculated and compared correctly
+        # as if the sender has sent a hashed 0-length payload body.
+        self.receive(content=None,
+                     content_type='text/plain',
+                     sender_kw=dict(content='',
+                                    content_type='text/plain'))
+
+    @raises(MissingContent)
     def test_cannot_receive_empty_content_only(self):
         content_type = 'text/plain'
         self.receive(sender_kw=dict(content='<content>',
                                     content_type=content_type),
-                     content=None, content_type=content_type)
+                     content=EmptyValue, content_type=content_type)
 
-    @raises(ValueError)
+    @raises(MissingContent)
     def test_cannot_receive_empty_content_type_only(self):
         content = '<content>'
         self.receive(sender_kw=dict(content=content,
                                     content_type='text/plain'),
-                     content=content, content_type=None)
+                     content=content, content_type=EmptyValue)
 
     @raises(MisComputedContentHash)
     def test_receive_wrong_content_type(self):
@@ -684,19 +859,24 @@
         expected = 
'123456\\1356420707\\kscxwNR2tJpP1T1zDLNPbB5UiKIU9tOSJXTUdG7X9h8=\\xandyandz'
         eq_(b64decode(bewit).decode('ascii'), expected)
 
-    def test_bewit_with_ext_and_backslashes(self):
-        credentials = self.credentials
-        credentials['id'] = '123\\456'
+    @raises(BadHeaderValue)
+    def test_bewit_with_invalid_ext(self):
         res = Resource(url='https://example.com/somewhere/over/the/rainbow',
                        method='GET', credentials=self.credentials,
                        timestamp=1356420407 + 300,
                        nonce='',
-                       ext='xand\\yandz'
-                       )
-        bewit = get_bewit(res)
+                       ext='xand\\yandz')
+        get_bewit(res)
 
-        expected = 
'123456\\1356420707\\b82LLIxG5UDkaChLU953mC+SMrbniV1sb8KiZi9cSsc=\\xand\\yandz'
-        eq_(b64decode(bewit).decode('ascii'), expected)
+    @raises(BadHeaderValue)
+    def test_bewit_with_backslashes_in_id(self):
+        credentials = self.credentials
+        credentials['id'] = '123\\456'
+        res = Resource(url='https://example.com/somewhere/over/the/rainbow',
+                       method='GET', credentials=self.credentials,
+                       timestamp=1356420407 + 300,
+                       nonce='')
+        get_bewit(res)
 
     def test_bewit_with_port(self):
         res = 
Resource(url='https://example.com:8080/somewhere/over/the/rainbow',
@@ -728,8 +908,8 @@
         url = 
"https://example.com/somewhere/over/the/rainbow?bewit={bewit}".format(bewit=bewit)
 
         raw_bewit, stripped_url = strip_bewit(url)
-        self.assertEquals(raw_bewit, bewit)
-        self.assertEquals(stripped_url, 
"https://example.com/somewhere/over/the/rainbow";)
+        self.assertEqual(raw_bewit, bewit)
+        self.assertEqual(stripped_url, 
"https://example.com/somewhere/over/the/rainbow";)
 
     @raises(InvalidBewit)
     def test_strip_url_without_bewit(self):
@@ -740,28 +920,25 @@
         bewit = 
b'123456\\1356420707\\IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=\\'
         bewit = urlsafe_b64encode(bewit).decode('ascii')
         bewit = parse_bewit(bewit)
-        self.assertEquals(bewit.id, '123456')
-        self.assertEquals(bewit.expiration, '1356420707')
-        self.assertEquals(bewit.mac, 
'IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=')
-        self.assertEquals(bewit.ext, '')
+        self.assertEqual(bewit.id, '123456')
+        self.assertEqual(bewit.expiration, '1356420707')
+        self.assertEqual(bewit.mac, 
'IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=')
+        self.assertEqual(bewit.ext, '')
 
     def test_parse_bewit_with_ext(self):
         bewit = 
b'123456\\1356420707\\IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=\\xandyandz'
         bewit = urlsafe_b64encode(bewit).decode('ascii')
         bewit = parse_bewit(bewit)
-        self.assertEquals(bewit.id, '123456')
-        self.assertEquals(bewit.expiration, '1356420707')
-        self.assertEquals(bewit.mac, 
'IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=')
-        self.assertEquals(bewit.ext, 'xandyandz')
+        self.assertEqual(bewit.id, '123456')
+        self.assertEqual(bewit.expiration, '1356420707')
+        self.assertEqual(bewit.mac, 
'IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=')
+        self.assertEqual(bewit.ext, 'xandyandz')
 
+    @raises(InvalidBewit)
     def test_parse_bewit_with_ext_and_backslashes(self):
         bewit = 
b'123456\\1356420707\\IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=\\xand\\yandz'
         bewit = urlsafe_b64encode(bewit).decode('ascii')
-        bewit = parse_bewit(bewit)
-        self.assertEquals(bewit.id, '123456')
-        self.assertEquals(bewit.expiration, '1356420707')
-        self.assertEquals(bewit.mac, 
'IGYmLgIqLrCe8CxvKPs4JlWIA+UjWJJouwgARiVhCAg=')
-        self.assertEquals(bewit.ext, 'xand\\yandz')
+        parse_bewit(bewit)
 
     @raises(InvalidBewit)
     def test_parse_invalid_bewit_with_only_one_part(self):
@@ -793,6 +970,7 @@
         })
         self.assertTrue(check_bewit(url, credential_lookup=credential_lookup, 
now=1356420407 + 10))
 
+    @raises(InvalidBewit)
     def test_validate_bewit_with_ext_and_backslashes(self):
         bewit = 
b'123456\\1356420707\\b82LLIxG5UDkaChLU953mC+SMrbniV1sb8KiZi9cSsc=\\xand\\yandz'
         bewit = urlsafe_b64encode(bewit).decode('ascii')
@@ -800,7 +978,7 @@
         credential_lookup = self.make_credential_lookup({
             self.credentials['id']: self.credentials,
         })
-        self.assertTrue(check_bewit(url, credential_lookup=credential_lookup, 
now=1356420407 + 10))
+        check_bewit(url, credential_lookup=credential_lookup, now=1356420407 + 
10)
 
     @raises(TokenExpired)
     def test_validate_expired_bewit(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk/util.py 
new/mohawk-1.0.0/mohawk/util.py
--- old/mohawk-0.3.4/mohawk/util.py     2017-01-07 21:33:03.000000000 +0100
+++ new/mohawk-1.0.0/mohawk/util.py     2017-04-17 22:58:13.000000000 +0200
@@ -19,6 +19,8 @@
 
 
 HAWK_VER = 1
+HAWK_HEADER_RE = 
re.compile(r'(?P<key>\w+)=\"(?P<value>[^\"\\]*)\"\s*(?:,\s*|$)')
+MAX_LENGTH = 4096
 log = logging.getLogger(__name__)
 allowable_header_keys = set(['id', 'ts', 'tsm', 'nonce', 'hash',
                              'error', 'ext', 'mac', 'app', 'dlg'])
@@ -149,45 +151,42 @@
         'Hawk id="dh37fgj492je", ts="1367076201", nonce="NPHgnG", ext="and
         welcome!", mac="CeWHy4d9kbLGhDlkyw2Nh3PJ7SDOdZDa267KH4ZaNMY="'
     """
-    attributes = {}
+    if len(auth_header) > MAX_LENGTH:
+        raise BadHeaderValue('Header exceeds maximum length of 
{max_length}'.format(
+            max_length=MAX_LENGTH))
 
     # Make sure we have a unicode object for consistency.
     if isinstance(auth_header, six.binary_type):
         auth_header = auth_header.decode('utf8')
 
-    parts = auth_header.split(',')
-    auth_scheme_parts = parts[0].split(' ')
-    if 'hawk' != auth_scheme_parts[0].lower():
+    scheme, attributes_string = auth_header.split(' ', 1)
+
+    if scheme.lower() != 'hawk':
         raise HawkFail("Unknown scheme '{scheme}' when parsing header"
-                       .format(scheme=auth_scheme_parts[0].lower()))
+                       .format(scheme=scheme))
+
 
-    # Replace 'Hawk key: value' with 'key: value'
-    # which matches the rest of parts
-    parts[0] = auth_scheme_parts[1]
-
-    for part in parts:
-        attr_parts = part.split('=')
-        key = attr_parts[0].strip()
+    attributes = {}
+
+    def replace_attribute(match):
+        """Extract the next key="value"-pair in the header."""
+        key = match.group('key')
+        value = match.group('value')
         if key not in allowable_header_keys:
             raise HawkFail("Unknown Hawk key '{key}' when parsing header"
                            .format(key=key))
-
-        if len(attr_parts) > 2:
-            attr_parts[1] = '='.join(attr_parts[1:])
-
-        # Chop of quotation marks
-        value = attr_parts[1]
-
-        if attr_parts[1].find('"') == 0:
-            value = attr_parts[1][1:]
-
-        if value.find('"') > -1:
-            value = value[0:-1]
-
         validate_header_attr(value, name=key)
-        value = unescape_header_attr(value)
+        if key in attributes:
+            raise BadHeaderValue('Duplicate key in header: 
{key}'.format(key=key))
         attributes[key] = value
 
+    # Iterate over all the key="value"-pairs in the header, replace them with
+    # an empty string, and store the extracted attribute in the attributes
+    # dict. Correctly formed headers will then leave nothing unparsed ('').
+    unparsed_header = HAWK_HEADER_RE.sub(replace_attribute, attributes_string)
+    if unparsed_header != '':
+        raise BadHeaderValue("Couldn't parse Hawk header", unparsed_header)
+
     log.debug('parsed Hawk header: {header} into: \n{parsed}'
               .format(header=auth_header, parsed=pprint.pformat(attributes)))
     return attributes
@@ -222,7 +221,7 @@
 # Allowed value characters:
 # !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, "
 _header_attribute_chars = re.compile(
-    r"^[ a-zA-Z0-9_\!#\$%&'\(\)\*\+,\-\./\:;<\=>\?@\[\]\^`\{\|\}~\"\\]*$")
+    r"^[ a-zA-Z0-9_\!#\$%&'\(\)\*\+,\-\./\:;<\=>\?@\[\]\^`\{\|\}~]*$")
 
 
 def validate_header_attr(val, name=None):
@@ -232,36 +231,14 @@
                              .format(name=name or '?', val=repr(val)))
 
 
-def escape_header_attr(val):
-
-    # Ensure we are working with Unicode for consistency.
-    if isinstance(val, six.binary_type):
-        val = val.decode('utf8')
-
-    # Escape quotes and slash like the hawk reference code.
-    val = val.replace('\\', '\\\\')
-    val = val.replace('"', '\\"')
-    val = val.replace('\n', '\\n')
-    return val
-
-
-def unescape_header_attr(val):
-    # Un-do the hawk escaping.
-    val = val.replace('\\n', '\n')
-    val = val.replace('\\\\', '\\').replace('\\"', '"')
-    return val
-
-
 def prepare_header_val(val):
-    val = escape_header_attr(val)
+    if isinstance(val, six.binary_type):
+        val = val.decode('utf-8')
     validate_header_attr(val)
     return val
 
 
 def normalize_header_attr(val):
-    if not val:
-        val = ''
-
-    # Normalize like the hawk reference code.
-    val = escape_header_attr(val)
+    if isinstance(val, six.binary_type):
+        return val.decode('utf-8')
     return val
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/mohawk.egg-info/PKG-INFO 
new/mohawk-1.0.0/mohawk.egg-info/PKG-INFO
--- old/mohawk-0.3.4/mohawk.egg-info/PKG-INFO   2017-01-07 21:39:20.000000000 
+0100
+++ new/mohawk-1.0.0/mohawk.egg-info/PKG-INFO   2019-01-09 23:01:05.000000000 
+0100
@@ -1,12 +1,23 @@
 Metadata-Version: 1.1
 Name: mohawk
-Version: 0.3.4
+Version: 1.0.0
 Summary: Library for Hawk HTTP authorization
 Home-page: https://github.com/kumar303/mohawk
 Author: Kumar McMillan, Austin King
 Author-email: [email protected]
 License: MPL 2.0 (Mozilla Public License)
-Description: UNKNOWN
+Description: 
+        Hawk lets two parties securely communicate with each other using
+        messages signed by a shared key.
+        It is based on HTTP MAC access authentication (which
+        was based on parts of OAuth 1.0).
+        
+        The Mohawk API is a little different from that of the Node library
+        (i.e. https://github.com/hueniverse/hawk).
+        It was redesigned to be more intuitive to developers, less prone to 
security problems, and more Pythonic.
+        
+        Read more: https://github.com/kumar303/mohawk/
+              
 Platform: UNKNOWN
 Classifier: Intended Audience :: Developers
 Classifier: Natural Language :: English
@@ -15,5 +26,8 @@
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 2.6
 Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
 Classifier: Topic :: Internet :: WWW/HTTP
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/setup.cfg new/mohawk-1.0.0/setup.cfg
--- old/mohawk-0.3.4/setup.cfg  2017-01-07 21:39:21.000000000 +0100
+++ new/mohawk-1.0.0/setup.cfg  2019-01-09 23:01:05.000000000 +0100
@@ -1,5 +1,4 @@
 [egg_info]
 tag_build = 
 tag_date = 0
-tag_svn_revision = 0
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mohawk-0.3.4/setup.py new/mohawk-1.0.0/setup.py
--- old/mohawk-0.3.4/setup.py   2017-01-07 21:34:20.000000000 +0100
+++ new/mohawk-1.0.0/setup.py   2019-01-09 22:59:04.000000000 +0100
@@ -2,9 +2,20 @@
 
 
 setup(name='mohawk',
-      version='0.3.4',
+      version='1.0.0',
       description="Library for Hawk HTTP authorization",
-      long_description='',
+      long_description="""
+Hawk lets two parties securely communicate with each other using
+messages signed by a shared key.
+It is based on HTTP MAC access authentication (which
+was based on parts of OAuth 1.0).
+
+The Mohawk API is a little different from that of the Node library
+(i.e. https://github.com/hueniverse/hawk).
+It was redesigned to be more intuitive to developers, less prone to security 
problems, and more Pythonic.
+
+Read more: https://github.com/kumar303/mohawk/
+      """,
       author='Kumar McMillan, Austin King',
       author_email='[email protected]',
       license='MPL 2.0 (Mozilla Public License)',
@@ -18,7 +29,10 @@
           'Programming Language :: Python :: 3',
           'Programming Language :: Python :: 2.6',
           'Programming Language :: Python :: 2.7',
-          'Programming Language :: Python :: 3.3',
+          'Programming Language :: Python :: 3.4',
+          'Programming Language :: Python :: 3.5',
+          'Programming Language :: Python :: 3.6',
+          'Programming Language :: Python :: 3.7',
           'Topic :: Internet :: WWW/HTTP',
       ],
       packages=find_packages(exclude=['tests']),


Reply via email to