Skip to content

Commit

Permalink
Merge 5841c80 into 2523b1d
Browse files Browse the repository at this point in the history
  • Loading branch information
Natim committed Oct 12, 2018
2 parents 2523b1d + 5841c80 commit b8f8bc6
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 26 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -11,6 +11,10 @@ This document describes changes between each past release.

- Raise a specific `CollectionNotFound` exception rather than a generic `KintoException`.

**Bug fixes**

- Handle json date and datetime object dumps.

**Internal changes**

- Update tests to work with Kinto 11.0.0
Expand Down
4 changes: 2 additions & 2 deletions kinto_http/batch.py
@@ -1,4 +1,3 @@
import json
import logging
from collections import defaultdict

Expand Down Expand Up @@ -28,6 +27,7 @@ def request(self, method, endpoint, data=None, permissions=None,
if permissions is not None:
payload['permissions'] = permissions

print(method, endpoint, payload, headers)
self.requests.append((method, endpoint, payload, headers))
# This is the signature of the session request.
return defaultdict(dict), defaultdict(dict)
Expand Down Expand Up @@ -75,7 +75,7 @@ def send(self):

# Full log in DEBUG mode
logger.debug("\nBatch #{}: \n\tRequest: {}\n\tResponse: {}\n".format(
id_request, json.dumps(chunk[i]), json.dumps(response)))
id_request, utils.json_dumps(chunk[i]), utils.json_dumps(response)))

# Raise in case of a 500
if status_code >= 500:
Expand Down
26 changes: 16 additions & 10 deletions kinto_http/session.py
Expand Up @@ -70,6 +70,17 @@ def request(self, method, endpoint, data=None, permissions=None,
if self.auth is not None:
kwargs.setdefault('auth', self.auth)

if kwargs.get('headers') is None:
kwargs['headers'] = dict()

if not isinstance(kwargs['headers'], dict):
raise TypeError("headers must be a dict (got {})".format(kwargs['headers']))

# Set the default User-Agent if not already defined.
# In the meantime, clone the header dict to avoid changing the
# user header dict when adding information.
kwargs['headers'] = {"User-Agent": USER_AGENT, **kwargs["headers"]}

payload = payload or {}
if data is not None:
payload['data'] = data
Expand All @@ -78,16 +89,11 @@ def request(self, method, endpoint, data=None, permissions=None,
permissions = permissions.as_dict()
payload['permissions'] = permissions
if method not in ('get', 'head'):
payload_kwarg = 'data' if 'files' in kwargs else 'json'
kwargs.setdefault(payload_kwarg, payload)

# Set the default User-Agent if not already defined.
if 'headers' not in kwargs or kwargs['headers'] is None:
kwargs['headers'] = {}
if not isinstance(kwargs['headers'], dict):
raise TypeError("headers must be a dict (got {})".format(kwargs['headers']))

kwargs['headers'] = {"User-Agent": USER_AGENT, **kwargs['headers']}
if 'files' in kwargs:
kwargs.setdefault('data', payload)
else:
kwargs.setdefault('data', utils.json_dumps(payload))
kwargs['headers'].setdefault('Content-Type', 'application/json')

retry = self.nb_retry
while retry >= 0:
Expand Down
1 change: 1 addition & 0 deletions kinto_http/tests/functional.py
Expand Up @@ -216,6 +216,7 @@ def test_collection_creation(self):

def test_collection_not_found(self):
self.client.create_bucket(id='mozilla')

with pytest.raises(CollectionNotFound):
self.client.get_collection(id='payments', bucket='mozilla')

Expand Down
62 changes: 48 additions & 14 deletions kinto_http/tests/test_session.py
Expand Up @@ -4,6 +4,7 @@
import time
import unittest
from unittest import mock
from datetime import date, datetime

from kinto_http.session import Session, create_session
from kinto_http.exceptions import KintoException, BackoffException
Expand All @@ -13,7 +14,6 @@

def fake_response(status_code):
response = mock.MagicMock()
response.headers = {'User-Agent': USER_AGENT}
response.status_code = status_code
return response

Expand All @@ -23,6 +23,9 @@ def setUp(self):
p = mock.patch('kinto_http.session.requests')
self.requests_mock = p.start()
self.addCleanup(p.stop)
self.requests_mock.request.headers = {'User-Agent': USER_AGENT}
self.requests_mock.request.post_json_headers = {'User-Agent': USER_AGENT,
'Content-Type': 'application/json'}

def test_uses_specified_server_url(self):
session = Session(mock.sentinel.server_url)
Expand All @@ -36,15 +39,14 @@ def test_no_auth_is_used_by_default(self):
session.request('get', '/test')
self.requests_mock.request.assert_called_with(
'get', 'https://example.org/test',
headers=self.requests_mock.request.return_value.headers)
headers=self.requests_mock.request.headers)

def test_bad_http_status_raises_exception(self):
response = fake_response(400)
self.requests_mock.request.return_value = response
session = Session('https://example.org')

self.assertRaises(KintoException, session.request, 'get', '/test',
headers=self.requests_mock.request.return_value.headers)
self.assertRaises(KintoException, session.request, 'get', '/test')

def test_bad_http_status_raises_exception_even_in_case_of_invalid_json_response(self):
response = fake_response(502)
Expand All @@ -65,7 +67,7 @@ def test_session_injects_auth_on_requests(self):
session.request('get', '/test')
self.requests_mock.request.assert_called_with(
'get', 'https://example.org/test',
auth=mock.sentinel.auth, headers=self.requests_mock.request.return_value.headers)
auth=mock.sentinel.auth, headers=self.requests_mock.request.headers)

def test_requests_arguments_are_forwarded(self):
response = fake_response(200)
Expand All @@ -75,7 +77,7 @@ def test_requests_arguments_are_forwarded(self):
foo=mock.sentinel.bar)
self.requests_mock.request.assert_called_with(
'get', 'https://example.org/test',
foo=mock.sentinel.bar, headers=self.requests_mock.request.return_value.headers)
foo=mock.sentinel.bar, headers=self.requests_mock.request.headers)

def test_raises_exception_if_headers_not_dict(self):
session = Session('https://example.org')
Expand All @@ -91,7 +93,8 @@ def test_passed_data_is_encoded_to_json(self):
data={'foo': 'bar'})
self.requests_mock.request.assert_called_with(
'post', 'https://example.org/test',
json={"data": {'foo': 'bar'}}, headers=self.requests_mock.request.return_value.headers)
data='{"data": {"foo": "bar"}}',
headers=self.requests_mock.request.post_json_headers)

def test_passed_data_is_passed_as_is_when_files_are_posted(self):
response = fake_response(200)
Expand All @@ -104,7 +107,7 @@ def test_passed_data_is_passed_as_is_when_files_are_posted(self):
'post', 'https://example.org/test',
data={"data": '{"foo": "bar"}'},
files={"attachment": {"filename"}},
headers=self.requests_mock.request.return_value.headers)
headers=self.requests_mock.request.headers)

def test_passed_permissions_is_added_in_the_payload(self):
response = fake_response(200)
Expand All @@ -116,8 +119,8 @@ def test_passed_permissions_is_added_in_the_payload(self):
permissions=permissions)
self.requests_mock.request.assert_called_with(
'post', 'https://example.org/test',
json={'permissions': {'foo': 'bar'}},
headers=self.requests_mock.request.return_value.headers)
data='{"permissions": {"foo": "bar"}}',
headers=self.requests_mock.request.post_json_headers)

def test_url_is_used_if_schema_is_present(self):
response = fake_response(200)
Expand All @@ -128,7 +131,7 @@ def test_url_is_used_if_schema_is_present(self):
session.request('get', 'https://example.org/anothertest')
self.requests_mock.request.assert_called_with(
'get', 'https://example.org/anothertest',
headers=self.requests_mock.request.return_value.headers)
headers=self.requests_mock.request.headers)

def test_creation_fails_if_session_and_server_url(self):
self.assertRaises(
Expand Down Expand Up @@ -170,16 +173,16 @@ def test_no_payload_is_sent_on_get_requests(self):
session.request('get', 'https://example.org/anothertest')
self.requests_mock.request.assert_called_with(
'get', 'https://example.org/anothertest',
headers=self.requests_mock.request.return_value.headers)
headers=self.requests_mock.request.headers)

def test_payload_is_sent_on_put_requests(self):
response = fake_response(200)
self.requests_mock.request.return_value = response
session = Session('https://example.org')
session.request('put', 'https://example.org/anothertest')
self.requests_mock.request.assert_called_with(
'put', 'https://example.org/anothertest', json={},
headers=self.requests_mock.request.return_value.headers)
'put', 'https://example.org/anothertest', data='{}',
headers=self.requests_mock.request.post_json_headers)

def test_user_agent_is_sent_on_requests(self):
response = fake_response(200)
Expand All @@ -200,6 +203,37 @@ def test_user_agent_contains_kinto_http_as_well_as_requests_and_python_versions(
assert python_info == 'python/{}'.format(python_version)


class SessionJSONTest(unittest.TestCase):
def setUp(self):
p = mock.patch('kinto_http.session.requests')
self.requests_mock = p.start()
self.addCleanup(p.stop)
self.requests_mock.request.headers = {'User-Agent': USER_AGENT}
self.requests_mock.request.post_json_headers = {'User-Agent': USER_AGENT,
'Content-Type': 'application/json'}
self.requests_mock.request.return_value = fake_response(200)
self.session = Session('https://example.org')

def test_passed_datetime_data_is_encoded_to_json(self):
self.session.request('post', '/test', data={'foo': datetime(2018, 6, 22, 18, 00)})
self.requests_mock.request.assert_called_with(
'post', 'https://example.org/test',
data='{"data": {"foo": "2018-06-22T18:00:00"}}',
headers=self.requests_mock.request.post_json_headers)

def test_passed_random_python_data_fails_to_be_encoded_to_json(self):
with pytest.raises(TypeError) as exc:
self.session.request('post', '/test', data={'foo': object()})
assert str(exc.value) == "Type <class 'object'> is not serializable"

def test_passed_date_data_is_encoded_to_json(self):
self.session.request('post', '/test', data={'foo': date(2018, 6, 22)})
self.requests_mock.request.assert_called_with(
'post', 'https://example.org/test',
data='{"data": {"foo": "2018-06-22"}}',
headers=self.requests_mock.request.post_json_headers)


class RetryRequestTest(unittest.TestCase):

def setUp(self):
Expand Down
13 changes: 13 additions & 0 deletions kinto_http/utils.py
@@ -1,5 +1,8 @@
import functools
import json
import re
import unicodedata
from datetime import date, datetime
from unidecode import unidecode


Expand Down Expand Up @@ -44,3 +47,13 @@ def chunks(l, n):
yield l[i:i+n]
else:
yield l


def json_iso_datetime(obj):
"""JSON serializer for objects not serializable by default json code"""
if isinstance(obj, (datetime, date)):
return obj.isoformat()
raise TypeError("Type %s is not serializable" % type(obj))


json_dumps = functools.partial(json.dumps, default=json_iso_datetime)

0 comments on commit b8f8bc6

Please sign in to comment.