Repository: brettcannon/gidgethub
Pull request #57 (2018-06-13 02:47:47)

``gidgethub.sansio.RateLimit.from_http`` returns ``None`` if ratelimi…
…t is not found in the headers.

Closes https://github.com/brettcannon/gidgethub/issues/25

codecov[bot]: # [Codecov](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=h1) Report
> Merging [#57](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=desc) into [master](https://codecov.io/gh/brettcannon/gidgethub/commit/4d5819cf50f3ad896713364d5e749bea466b9f05?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/brettcannon/gidgethub/pull/57/graphs/tree.svg?width=650&height=150&src=pr&token=t1zJsj2sZH)](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master    #57   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          14     14           
  Lines        1030   1046   +16     
  Branches       58     58           
=====================================
+ Hits         1030   1046   +16
```


| [Impacted Files](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [gidgethub/test/test\_sansio.py](https://codecov.io/gh/brettcannon/gidgethub/pull/57/diff?src=pr&el=tree#diff-Z2lkZ2V0aHViL3Rlc3QvdGVzdF9zYW5zaW8ucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [gidgethub/sansio.py](https://codecov.io/gh/brettcannon/gidgethub/pull/57/diff?src=pr&el=tree#diff-Z2lkZ2V0aHViL3NhbnNpby5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=footer). Last update [4d5819c...e1ff603](https://codecov.io/gh/brettcannon/gidgethub/pull/57?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

brettcannon: Only a minor thing to tweak to make the code more obvious in one case.

I've also invited you to be a contributor on the repo so you don't have to wait for me to do merges on obvious stuff like your `3.7-dev` PR. 😄 

Patch:
diff --git a/README.rst b/README.rst
index fd36c90..733194e 100644
--- a/README.rst
+++ b/README.rst
@@ -61,6 +61,9 @@ Changelog
 2.5.1 (in development)
 ''''''''''''''''''''''
 
+- ``gidgethub.sansio.RateLimit.from_http`` returns ``None`` if ratelimit is
+  not found in the headers.
+
 2.5.0
 '''''
 
diff --git a/docs/sansio.rst b/docs/sansio.rst
index b44e073..e27bd79 100644
--- a/docs/sansio.rst
+++ b/docs/sansio.rst
@@ -184,7 +184,7 @@ that are provided to you. Continuing from the example in the Requests_ section::
         The maximum limit of requests per hour the requester can make.
 
 
-    .. attribute: remaining
+    .. attribute:: remaining
 
         How many requests are left for the request until their quota is reset.
 
@@ -195,10 +195,10 @@ that are provided to you. Continuing from the example in the Requests_ section::
         quota is refreshed. The object is timezone-aware to UTC.
 
 
-    .. classmethod: from_http(headers)
+    .. classmethod:: from_http(headers)
 
         Create a :class:`RateLimit` instance from the HTTP headers of a GitHub API
-        response.
+        response.  Returns ``None`` if the ratelimit is not found in the headers.
 
 
 .. function:: decipher_response(status_code, headers, body)
diff --git a/gidgethub/sansio.py b/gidgethub/sansio.py
index b8b85ed..e26a381 100644
--- a/gidgethub/sansio.py
+++ b/gidgethub/sansio.py
@@ -222,16 +222,20 @@ class RateLimit:
         return f"< {self.remaining:,}/{self.limit:,} until {self.reset_datetime} >"
 
     @classmethod
-    def from_http(cls, headers: Mapping) -> "RateLimit":
+    def from_http(cls, headers: Mapping) -> Optional["RateLimit"]:
         """Gather rate limit information from HTTP headers.
 
         The mapping providing the headers is expected to support lowercase
-        keys.
+        keys.  Returns ``None`` if ratelimit info is not found in the headers.
         """
-        limit = int(headers["x-ratelimit-limit"])
-        remaining = int(headers["x-ratelimit-remaining"])
-        reset_epoch = float(headers["x-ratelimit-reset"])
-        return cls(limit=limit, remaining=remaining, reset_epoch=reset_epoch)
+        try:
+            limit = int(headers["x-ratelimit-limit"])
+            remaining = int(headers["x-ratelimit-remaining"])
+            reset_epoch = float(headers["x-ratelimit-reset"])
+        except KeyError:
+            return None
+        else:
+            return cls(limit=limit, remaining=remaining, reset_epoch=reset_epoch)
 
 
 _link_re = re.compile(r'\<(?P<uri>[^>]+)\>;\s*'
@@ -251,7 +255,7 @@ def _next_link(link: Optional[str]) -> Optional[str]:
 
 
 def decipher_response(status_code: int, headers: Mapping,
-                      body: bytes) -> Tuple[Any, RateLimit, Optional[str]]:
+                      body: bytes) -> Tuple[Any, Optional[RateLimit], Optional[str]]:
     """Decipher an HTTP response for a GitHub API request.
 
     The mapping providing the headers is expected to support lowercase keys.
@@ -287,7 +291,7 @@ def decipher_response(status_code: int, headers: Mapping,
             exc_type = BadRequest
             if status_code == 403:
                 rate_limit = RateLimit.from_http(headers)
-                if not rate_limit.remaining:
+                if rate_limit and not rate_limit.remaining:
                     raise RateLimitExceeded(rate_limit, message)
             elif status_code == 422:
                 errors = data.get("errors", None)


Test patch:
diff --git a/gidgethub/test/test_sansio.py b/gidgethub/test/test_sansio.py
index e38a4d7..dd3b023 100644
--- a/gidgethub/test/test_sansio.py
+++ b/gidgethub/test/test_sansio.py
@@ -218,6 +218,11 @@ class TestRateLimit:
         assert format(rate, ',') in message
         assert str(reset) in message
 
+    def test_from_http_no_ratelimit(self):
+        headers = {}
+        rate_limit = sansio.RateLimit.from_http(headers)
+        assert rate_limit is None
+
 
 def sample(directory, status_code):
     # pytest doesn't set __spec__.origin :(
@@ -394,6 +399,25 @@ class TestDecipherResponse:
         assert rate_limit.remaining == 43
         assert data.startswith("diff --git")
 
+    def test_no_ratelimit(self):
+        """Test no ratelimit in headers."""
+        status_code = 201
+        headers = {"content-type": "application/json; charset=utf-8",
+                   "link": "<http://example.com>; test=\"unimportant\""}
+        data = {
+            "id": 208045946,
+            "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
+            "name": "bug",
+            "color": "f29513",
+            "default": True
+        }
+        body = json.dumps(data).encode("UTF-8")
+        returned_data, rate_limit, more = sansio.decipher_response(status_code,
+                                                                   headers, body)
+        assert more is None
+        assert rate_limit is None
+        assert returned_data == data
+
 
 class TestFormatUrl: