Skip to content

Commit e8f4025

Browse files
authored
Provide json.dumps an encoder for datetime (#15)
- add util wrapper for json.dumps - declares default encoder for datetime, datetime.date values - tests and fixes #14
1 parent 2b82f3e commit e8f4025

13 files changed

+217
-134
lines changed

labkey/domain.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from __future__ import unicode_literals
1717
import json
1818

19-
from labkey.utils import ServerContext
19+
from labkey.utils import json_dumps, ServerContext
2020

2121

2222
def strip_none_values(data, do_strip=True):
@@ -326,7 +326,7 @@ def create(server_context, domain_definition, container_path=None):
326326

327327
domain = None
328328

329-
raw_domain = server_context.make_request(url, json.dumps(domain_definition), headers=headers)
329+
raw_domain = server_context.make_request(url, json_dumps(domain_definition), headers=headers)
330330

331331
if raw_domain is not None:
332332
domain = Domain.from_data(raw_domain)
@@ -355,7 +355,7 @@ def drop(server_context, schema_name, query_name, container_path=None):
355355
'queryName': query_name
356356
}
357357

358-
return server_context.make_request(url, json.dumps(payload), headers=headers)
358+
return server_context.make_request(url, json_dumps(payload), headers=headers)
359359

360360

361361
def get(server_context, schema_name, query_name, container_path=None):
@@ -431,4 +431,4 @@ def save(server_context, schema_name, query_name, domain, container_path=None):
431431
'schemaName': schema_name
432432
}
433433

434-
return server_context.make_request(url, json.dumps(payload), headers=headers)
434+
return server_context.make_request(url, json_dumps(payload), headers=headers)

labkey/experiment.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
# limitations under the License.
1515
#
1616
from __future__ import unicode_literals
17-
import json
1817

19-
from labkey.utils import ServerContext
18+
from labkey.utils import json_dumps, ServerContext
2019

2120

2221
# TODO Incorporate logging
@@ -42,7 +41,7 @@ def load_batch(server_context, assay_id, batch_id):
4241
'Accept': 'text/plain'
4342
}
4443

45-
json_body = server_context.make_request(load_batch_url, json.dumps(payload, sort_keys=True), headers=headers)
44+
json_body = server_context.make_request(load_batch_url, json_dumps(payload, sort_keys=True), headers=headers)
4645
if json_body is not None:
4746
loaded_batch = Batch.from_data(json_body['batch'])
4847

@@ -95,7 +94,7 @@ def save_batches(server_context, assay_id, batches):
9594
'Accept': 'text/plain'
9695
}
9796

98-
json_body = server_context.make_request(save_batch_url, json.dumps(payload, sort_keys=True), headers=headers)
97+
json_body = server_context.make_request(save_batch_url, json_dumps(payload, sort_keys=True), headers=headers)
9998
if json_body is not None:
10099
resp_batches = json_body['batches']
101100
return [Batch.from_data(resp_batch) for resp_batch in resp_batches]

labkey/query.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
############################################################################
4242
"""
4343
from __future__ import unicode_literals
44-
import json
44+
45+
from labkey.utils import json_dumps
4546

4647
_query_headers = {
4748
'Content-Type': 'application/json'
@@ -80,7 +81,7 @@ def delete_rows(server_context, schema_name, query_name, rows, container_path=No
8081
'rows': rows
8182
}
8283

83-
return server_context.make_request(url, json.dumps(payload, sort_keys=True), headers=_query_headers, timeout=timeout)
84+
return server_context.make_request(url, json_dumps(payload, sort_keys=True), headers=_query_headers, timeout=timeout)
8485

8586

8687
def execute_sql(server_context, schema_name, sql, container_path=None,
@@ -161,7 +162,7 @@ def insert_rows(server_context, schema_name, query_name, rows, container_path=No
161162
'rows': rows
162163
}
163164

164-
return server_context.make_request(url, json.dumps(payload, sort_keys=True), headers=_query_headers,
165+
return server_context.make_request(url, json_dumps(payload, sort_keys=True), headers=_query_headers,
165166
timeout=timeout)
166167

167168

@@ -281,7 +282,7 @@ def update_rows(server_context, schema_name, query_name, rows, container_path=No
281282
'rows': rows
282283
}
283284

284-
return server_context.make_request(url, json.dumps(payload, sort_keys=True), headers=_query_headers,
285+
return server_context.make_request(url, json_dumps(payload, sort_keys=True), headers=_query_headers,
285286
timeout=timeout)
286287

287288

labkey/unsupported/wiki.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"""
2424
from __future__ import unicode_literals
2525

26-
import json
2726
from requests.exceptions import SSLError
2827

28+
from labkey.utils import json_dumps
29+
2930

3031
def update_wiki(server_context, wiki_name, wiki_body, container_path=None):
3132
"""
@@ -100,7 +101,7 @@ def update_wiki(server_context, wiki_name, wiki_body, container_path=None):
100101
wiki_vars['body'] = wiki_body
101102

102103
try:
103-
data = server_context.make_request(update_wiki_url, payload=json.dumps(wiki_vars, sort_keys=True),
104+
data = server_context.make_request(update_wiki_url, payload=json_dumps(wiki_vars, sort_keys=True),
104105
headers=headers, non_json_response=True)
105106
except SSLError as e:
106107
print("There was a problem while attempting to submit the read for the wiki page '" + str(wiki_name) + "' via the URL " + str(e.geturl()) + ". The HTTP response code was " + str(e.getcode()))

labkey/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from __future__ import unicode_literals
1717

1818
import requests
19+
import json
20+
from functools import wraps
21+
from datetime import date, datetime
1922

2023
from requests.exceptions import RequestException
2124
from labkey.exceptions import RequestError, RequestAuthorizationError, QueryNotFoundError, ServerContextError, \
@@ -179,3 +182,18 @@ def handle_response(response, non_json_response=False):
179182
else:
180183
# consider response.raise_for_status()
181184
raise RequestError(response)
185+
186+
187+
# Issue #14: json.dumps on datetime throws TypeError
188+
class DateTimeEncoder(json.JSONEncoder):
189+
def default(self, o):
190+
if isinstance(o, (datetime, date)):
191+
return o.isoformat()
192+
193+
return super(DateTimeEncoder, self).default(o)
194+
195+
196+
@wraps(json.dumps)
197+
def json_dumps(*args, **kwargs):
198+
kwargs.setdefault('cls', DateTimeEncoder)
199+
return json.dumps(*args, **kwargs)

test/test_domain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from labkey.domain import create, Domain, drop, get, infer_fields, save
2929
from labkey.exceptions import RequestAuthorizationError
3030

31-
from test_utils import MockLabKey, mock_server_context, success_test, success_test_get, throws_error_test, throws_error_test_get
31+
from utilities import MockLabKey, mock_server_context, success_test, success_test_get, throws_error_test, throws_error_test_get
3232

3333

3434
domain_controller = 'property'

test/test_experiment_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from labkey.experiment import load_batch, save_batch, Batch, Run
2626
from labkey.exceptions import RequestError, QueryNotFoundError, ServerNotFoundError, RequestAuthorizationError
2727

28-
from test_utils import MockLabKey, mock_server_context, success_test, throws_error_test
28+
from utilities import MockLabKey, mock_server_context, success_test, throws_error_test
2929

3030

3131
class MockLoadBatch(MockLabKey):

test/test_labkey.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from test_query_api import suite as query_suite
2323
from test_security import suite as security_suite
2424
from test_unsupported import suite as unsupported_suite
25+
from test_utils import suite as utils_suite
2526

2627
if __name__ == '__main__':
2728

@@ -35,6 +36,7 @@
3536
exp_suite(),
3637
query_suite(),
3738
security_suite(),
38-
unsupported_suite()
39+
unsupported_suite(),
40+
utils_suite()
3941
])
4042
unittest.TextTestRunner().run(all_tests)

test/test_query_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from labkey.query import delete_rows, update_rows, insert_rows, select_rows, execute_sql
2626
from labkey.exceptions import RequestError, QueryNotFoundError, ServerNotFoundError, RequestAuthorizationError
2727

28-
from test_utils import MockLabKey, mock_server_context, success_test, throws_error_test
28+
from utilities import MockLabKey, mock_server_context, success_test, throws_error_test
2929

3030

3131
class MockSelectRows(MockLabKey):

test/test_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
remove_from_group, remove_from_role, add_to_role, get_roles, list_groups
2828
from labkey.exceptions import RequestError, QueryNotFoundError, ServerNotFoundError, RequestAuthorizationError
2929

30-
from test_utils import MockLabKey, mock_server_context, success_test, throws_error_test
30+
from utilities import MockLabKey, mock_server_context, success_test, throws_error_test
3131

3232

3333
class MockSecurityController(MockLabKey):

test/test_unsupported.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from labkey import utils
2020
from labkey.unsupported import messageboard
2121

22-
from test_utils import MockLabKey, mock_server_context, success_test
22+
from utilities import MockLabKey, mock_server_context, success_test
2323

2424

2525
class MockPostMessage(MockLabKey):

test/test_utils.py

Lines changed: 31 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2017 LabKey Corporation
2+
# Copyright (c) 2018 LabKey Corporation
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -14,132 +14,49 @@
1414
# limitations under the License.
1515
#
1616
from __future__ import unicode_literals
17-
import requests
17+
from datetime import date, datetime
18+
import unittest
1819

1920
try:
2021
import mock
2122
except ImportError:
2223
import unittest.mock as mock
2324

24-
from labkey.utils import create_server_context
25+
from labkey import utils
2526

2627

27-
def mock_server_context(mock_action):
28-
# mock the CSRF token
29-
with mock.patch('labkey.utils.requests.sessions.Session.get') as mock_get:
28+
class TestJsonDumps(unittest.TestCase):
3029

31-
mock_get.return_value = mock_action.get_csrf_response()
32-
return create_server_context(mock_action.server_name, mock_action.project_path, mock_action.context_path,
33-
api_key=mock_action.api_key)
30+
def test_encoder(self):
31+
# test date and datetime encoding
32+
payload = {
33+
'my_date': date(1985, 9, 11),
34+
'my_date_time': datetime(2018, 9, 18, 5, 25)
35+
}
3436

37+
utils.json_dumps(payload)
3538

36-
def success_test(test, expected_response, api_method, compare_response, *args, **expected_kwargs):
37-
with mock.patch('labkey.utils.requests.Session.post') as mock_post:
38-
mock_post.return_value = expected_response
39-
resp = api_method(*args)
39+
def test_encoder_override(self):
40+
payload = {
41+
'testdate': datetime(2018, 9, 11, 6, 45)
42+
}
4043

41-
# validate response is as expected
42-
if compare_response:
43-
test.assertEqual(resp, expected_response.text)
44+
try:
45+
# disable the "cls" override by passing None
46+
utils.json_dumps(payload, cls=None)
47+
except TypeError as e:
48+
if "is not JSON serializable" not in str(e):
49+
print("Did not see expected exception")
50+
raise e
4451

45-
# validate call is made as expected
46-
expected_args = expected_kwargs.pop('expected_args')
47-
mock_post.assert_called_once_with(*expected_args, **expected_kwargs)
4852

53+
def suite():
54+
load_tests = unittest.TestLoader().loadTestsFromTestCase
55+
return unittest.TestSuite([
56+
load_tests(TestJsonDumps)
57+
])
4958

50-
def success_test_get(test, expected_response, api_method, compare_response, *args, **expected_kwargs):
51-
with mock.patch('labkey.utils.requests.Session.get') as mock_get:
52-
mock_get.return_value = expected_response
53-
resp = api_method(*args)
5459

55-
# validate response is as expected
56-
if compare_response:
57-
test.assertEqual(resp, expected_response.text)
58-
59-
# validate call is made as expected
60-
expected_args = expected_kwargs.pop('expected_args')
61-
mock_get.assert_called_once_with(*expected_args, **expected_kwargs)
62-
63-
64-
def throws_error_test(test, expected_error, expected_response, api_method, *args, **expected_kwargs):
65-
with mock.patch('labkey.utils.requests.Session.post') as mock_post:
66-
with test.assertRaises(expected_error):
67-
mock_post.return_value = expected_response
68-
api_method(*args)
69-
70-
# validate call is made as expected
71-
expected_args = expected_kwargs.pop('expected_args')
72-
mock_post.assert_called_once_with(*expected_args, **expected_kwargs)
73-
74-
75-
def throws_error_test_get(test, expected_error, expected_response, api_method, *args, **expected_kwargs):
76-
with mock.patch('labkey.utils.requests.Session.get') as mock_get:
77-
with test.assertRaises(expected_error):
78-
mock_get.return_value = expected_response
79-
api_method(*args)
80-
81-
# validate call is made as expected
82-
expected_args = expected_kwargs.pop('expected_args')
83-
mock_get.assert_called_once_with(*expected_args, **expected_kwargs)
84-
85-
86-
class MockLabKey:
87-
api = ""
88-
default_protocol = 'https://'
89-
default_server = 'my_testServer:8080'
90-
default_context_path = 'testPath'
91-
default_project_path = 'testProject/subfolder'
92-
default_action = 'query'
93-
default_success_body = ''
94-
default_unauthorized_body = ''
95-
default_server_not_found_body = ''
96-
default_query_not_found_body = ''
97-
default_general_server_error_body = ''
98-
default_api_key = None
99-
100-
def __init__(self, **kwargs):
101-
self.protocol = kwargs.pop('protocol', self.default_protocol)
102-
self.server_name = kwargs.pop('server_name', self.default_server)
103-
self.context_path = kwargs.pop('context_path', self.default_context_path)
104-
self.project_path = kwargs.pop('project_path', self.default_project_path)
105-
self.action = kwargs.pop('action', self.default_action)
106-
self.success_body = kwargs.pop('success_body', self.default_success_body)
107-
self.unauthorized_body = kwargs.pop('unauthorized_body', self.default_unauthorized_body)
108-
self.server_not_found_body = kwargs.pop('server_not_found_body', self.default_server_not_found_body)
109-
self.query_not_found_body = kwargs.pop('query_not_found_body', self.default_query_not_found_body)
110-
self.general_server_error_body = kwargs.pop('general_server_error_body', self.default_general_server_error_body)
111-
self.api_key = kwargs.pop('api_key', self.default_api_key)
112-
113-
def _get_mock_response(self, code, url, body):
114-
mock_response = mock.Mock(requests.Response)
115-
mock_response.status_code = code
116-
mock_response.url = url
117-
mock_response.text = body
118-
mock_response.json.return_value = mock_response.text
119-
return mock_response
120-
121-
def get_server_url(self):
122-
return "{protocol}{server}/{context}/{container}/{action}-{api}"\
123-
.format(protocol=self.protocol, server=self.server_name, context=self.context_path,
124-
container=self.project_path, action=self.action, api=self.api)
125-
126-
def get_successful_response(self, code=200):
127-
return self._get_mock_response(code, self.get_server_url(), self.success_body)
128-
129-
def get_unauthorized_response(self, code=401):
130-
return self._get_mock_response(code, self.get_server_url(), self.unauthorized_body)
131-
132-
def get_server_not_found_response(self, code=404):
133-
response = self._get_mock_response(code, self.get_server_url(), self.server_not_found_body)
134-
# calling json() on empty response body causes a ValueError
135-
response.json.side_effect = ValueError()
136-
return response
137-
138-
def get_query_not_found_response(self, code=404):
139-
return self._get_mock_response(code, self.get_server_url(), self.query_not_found_body)
140-
141-
def get_general_error_response(self, code=500):
142-
return self._get_mock_response(code, self.get_server_url(), self.general_server_error_body)
143-
144-
def get_csrf_response(self, code=200):
145-
return self._get_mock_response(code, self.get_server_url(), {'CSRF': 'MockCSRF'})
60+
if __name__ == '__main__':
61+
utils.DISABLE_CSRF_CHECK = True
62+
unittest.main()

0 commit comments

Comments
 (0)