Skip to content

Commit 4398acb

Browse files
committed
Support serialization of UUID objects to JSON
Support UUID objects in get_lab_from_endpoint_id Fix syntax error in python publish workflow
1 parent 92438c1 commit 4398acb

File tree

6 files changed

+42
-9
lines changed

6 files changed

+42
-9
lines changed

.github/workflows/python-publish.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ jobs:
5454
- name: Publish distribution to PyPI
5555
# GitHub recommends pinning 3rd party actions to a commit SHA.
5656
uses: pypa/gh-action-pypi-publish@6f7e8d9c0b1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d
57-
with:
58-
user: __token__
59-
password: ${{ secrets.PYPI_API_TOKEN }}
57+
with:
58+
user: __token__
59+
password: ${{ secrets.PYPI_API_TOKEN }}

one/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""The Open Neurophysiology Environment (ONE) API."""
2-
__version__ = '3.0b3'
2+
__version__ = '3.0b4'

one/remote/globus.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ def get_lab_from_endpoint_id(endpoint=None, alyx=None):
358358
alyx = alyx or AlyxClient(silent=True)
359359
if not endpoint:
360360
endpoint = get_local_endpoint_id()
361-
lab = alyx.rest('labs', 'list', django=f'repositories__globus_endpoint_id,{endpoint}')
361+
lab = alyx.rest('labs', 'list', django=f'repositories__globus_endpoint_id,{str(endpoint)}')
362362
if len(lab):
363363
lab_names = [la['name'] for la in lab]
364364
return lab_names

one/tests/remote/test_globus.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ def test_get_lab_from_endpoint_id(self):
175175
self.assertEqual(len(globus.get_lab_from_endpoint_id('123', ac)), 2)
176176

177177
# Check behaviour when no input ID returned
178-
with mock.patch('one.remote.globus.get_local_endpoint_id', return_value=endpoint_id):
178+
uid = uuid.UUID(endpoint_id)
179+
with mock.patch('one.remote.globus.get_local_endpoint_id', return_value=uid):
179180
name = globus.get_lab_from_endpoint_id(alyx=ac)[0]
180181
self.assertEqual(name, 'mainenlab')
181182

one/tests/test_alyxclient.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import urllib.parse
55
import random
66
import weakref
7+
import uuid
78
import os
89
import one.webclient as wc
910
import one.params
@@ -170,7 +171,7 @@ def setUp(self):
170171
'users': [TEST_DB_1['username']],
171172
}) for _ in range(2)]
172173

173-
self.eids = [x['url'].split('/')[-1] for x in sessions]
174+
self.eids = [uuid.UUID(x['url'].split('/')[-1]) for x in sessions]
174175
self.endpoint = 'sessions'
175176
self.field_name = 'extended_qc'
176177
# We filter by key value so we use randint to avoid race condition in concurrent tests
@@ -253,6 +254,14 @@ def test_empty(self):
253254
modified = self.ac.json_field_remove_key(self.endpoint, eid, self.field_name)
254255
self.assertIsNone(modified)
255256

257+
def test_uuid_serialize(self):
258+
"""Check that UUID objects are serialized to JSON."""
259+
data = {'uid': self.eids[-1], **self.data_dict}
260+
written = self.ac.json_field_write(self.endpoint, self.eids[0], self.field_name, data)
261+
self.assertIsInstance(written, dict)
262+
# Encoder should have cast uuid to str
263+
self.assertEqual(str(self.eids[-1]), written.get('uid'))
264+
256265
def tearDown(self):
257266
self.ac.rest('subjects', 'delete', id=self.subj['nickname'])
258267

@@ -581,6 +590,17 @@ def _check_get_query(self, call_args, limit, offset):
581590
expected = {'foo': ['bar'], 'offset': [str(offset)], 'limit': [str(limit)]}
582591
self.assertDictEqual(query, expected)
583592

593+
def test_json_encoder(self):
594+
"""Test that the JSONEncoder subclass serializes UUID objects."""
595+
uid = uuid.uuid4()
596+
data = {'foo': 12, 'bar': uid}
597+
# First check that the default encoder raises;
598+
# python could add support for UUID objects in the future
599+
self.assertRaises(TypeError, json.dumps, data)
600+
serialized = json.dumps(data, cls=wc._JSONEncoder)
601+
expected = '{"foo": 12, "bar": "' + str(uid) + '"}'
602+
self.assertEqual(expected, serialized)
603+
584604

585605
if __name__ == '__main__':
586606
unittest.main(exit=False, verbosity=2)

one/webclient.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
>>> local_path = alyx.download_file(url, target_dir='zadorlab/Subjects/flowers/2018-07-13/1/')
3131
3232
"""
33+
from uuid import UUID
3334
import json
3435
import logging
3536
import math
@@ -62,6 +63,16 @@
6263
_logger = logging.getLogger(__name__)
6364

6465

66+
class _JSONEncoder(json.JSONEncoder):
67+
"""A JSON encoder that handles UUID objects."""
68+
69+
def default(self, o):
70+
"""Cast UUID objects to str before serializing."""
71+
if isinstance(o, UUID):
72+
return str(o)
73+
return super().default(o)
74+
75+
6576
def _cache_response(method):
6677
"""Decorator for the generic request method for caching REST reponses.
6778
@@ -139,7 +150,7 @@ def wrapper_decorator(alyx_client, *args, expires=None, clobber=False, **kwargs)
139150
_logger.debug('caching REST response')
140151
expiry_datetime = datetime.now() + (timedelta() if expires is True else expires)
141152
with open(rest_cache / name, 'w') as f:
142-
json.dump((response, expiry_datetime.isoformat()), f)
153+
json.dump((response, expiry_datetime.isoformat()), f, cls=_JSONEncoder)
143154
return response
144155

145156
return wrapper_decorator
@@ -646,7 +657,8 @@ def _generic_request(self, reqfunction, rest_query, data=None, files=None):
646657
_logger.debug(f'{self.base_url + rest_query}, headers: {self._headers}')
647658
headers = self._headers.copy()
648659
if files is None:
649-
data = json.dumps(data) if isinstance(data, dict) or isinstance(data, list) else data
660+
to_json = functools.partial(json.dumps, cls=_JSONEncoder)
661+
data = to_json(data) if isinstance(data, dict) or isinstance(data, list) else data
650662
headers['Content-Type'] = 'application/json'
651663
if rest_query.startswith('/docs'):
652664
# the mixed accept application may cause errors sometimes, only necessary for the docs

0 commit comments

Comments
 (0)