Merge lp:~mithrandi/txfluiddb/object-operations into lp:~txfluiddb-maint/txfluiddb/trunk-obsolete-1.6

Proposed by Tristan Seligmann
Status: Merged
Merged at revision: 3
Proposed branch: lp:~mithrandi/txfluiddb/object-operations
Merge into: lp:~txfluiddb-maint/txfluiddb/trunk-obsolete-1.6
Diff against target: None lines
To merge this branch: bzr merge lp:~mithrandi/txfluiddb/object-operations
Reviewer Review Type Date Requested Status
Tristan Seligmann Needs Resubmitting
Jonathan Jacobs (community) Needs Fixing
Review via email: mp+10500@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tristan Seligmann (mithrandi) wrote :

Implement operations on objects.

Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

  1. `Endpoint.submit`'s parameters need documentation, specifically the "data" parameter probably needs some careful and clear documentation.

  2. The `agent` parameter to `getPage` should probably include a version.

  3. There are some klaxons going off in `Object.get`. You test for the "valueType" key, and then read "valueEncoding", test it against a value and then create a blob with "valueType". It looks like "valueEncoding" was a brainfart. Also, why not use the "encoding" value (that you tested) as the parameter to `decode`? See point 5.

  4. `Object.query`'s return value is not documented.

  5. The description of the return value for `Object.getTags` is slightly confusing.

  6. There isn't any test coverage for the potential exception / errback in `Object.get`.

  7. `ObjectTests.test_getInteger` incorrectly documents the type as "integer", you probably meant "int".

  8. In "ObjectTests", `test_setTag`, `test_setBlob`, `test_createObjectWithAbout` have no docs.

  9. There are a handful of lines longer than 80 columns.

review: Needs Fixing
Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

> parameter to `decode`? See point 5.

I meant point 6.

13. By Tristan Seligmann <tristan@viridian>

Improve Endpoint.submit documentation.

14. By Tristan Seligmann <tristan@viridian>

Document Object.query return value.

15. By Tristan Seligmann <tristan@viridian>

Fix test docstring.

16. By Tristan Seligmann <tristan@viridian>

Tests for encoding stuff.

17. By Tristan Seligmann

Test unsupported encodings.

18. By Tristan Seligmann

Split long lines.

Revision history for this message
Tristan Seligmann (mithrandi) wrote :

I believe I've addressed all of these points except for point 8; I'm not sure how to write a useful docstring for those tests. Any suggestions?

Revision history for this message
Tristan Seligmann (mithrandi) wrote :

Testing something.

review: Needs Resubmitting
Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

> I believe I've addressed all of these points except for point 8; I'm not sure
> how to write a useful docstring for those tests. Any suggestions?

My only suggestion is the obvious one, attempt to concisely explain the desired behaviour of the code. What is supposed to happen? etc.

I noticed that several other tests are missing docstrings too.

19. By Tristan Seligmann

Docstrings.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'LICENSE'
2--- LICENSE 2009-08-17 23:05:23 +0000
3+++ LICENSE 2009-08-21 00:05:49 +0000
4@@ -1,4 +1,4 @@
5-Copyright (c) 2009 Tristan Seligmann
6+Copyright © 2009 Tristan Seligmann
7
8 Permission is hereby granted, free of charge, to any person obtaining
9 a copy of this software and associated documentation files (the
10
11=== modified file 'txfluiddb/client.py'
12--- txfluiddb/client.py 2009-08-19 20:33:58 +0000
13+++ txfluiddb/client.py 2009-08-21 00:42:26 +0000
14@@ -15,7 +15,7 @@
15 @type components: C{list} of C{unicode}
16 @ivar components: A list of path components for this identifier.
17
18- @type collectionName: C{str}
19+ @type collectionName: C{unicode}
20 @cvar collectionName: The top-level path component under which these
21 identifiers exist.
22
23@@ -66,13 +66,19 @@
24 return u'/'.join(self.components)
25
26
27- def getURL(self, endpoint, prefix=None):
28+ def getURL(self, endpoint, prefix=None, suffix=[]):
29 """
30 Get the URL for this path as accessed through the given endpoint.
31
32 @type endpoint: L{Endpoint}
33 @param endpoint: The endpoint to operate through.
34
35+ @type prefix: C{list} of C{unicode}
36+ @param prefix: A list of components to prepend before this path.
37+
38+ @type suffix: C{list} of C{unicode}
39+ @param suffix: A list of components to append after this path.
40+
41 @rtype: C{str}
42 """
43 if self.collectionName is None:
44@@ -83,12 +89,11 @@
45 components.append(self.collectionName)
46 else:
47 components.extend(prefix)
48-
49- for component in self.components:
50- encoded = component.encode('utf-8')
51- quoted = quote(encoded, safe='')
52- components.append(quoted)
53-
54+ components.extend(self.components)
55+ components.extend(suffix)
56+
57+ components = [quote(component.encode('utf-8'), safe='')
58+ for component in components]
59 return endpoint.getRootURL() + '/'.join(components)
60
61
62@@ -97,7 +102,7 @@
63 """
64 Representation of a FluidDB namespace.
65 """
66- collectionName = 'namespaces'
67+ collectionName = u'namespaces'
68
69
70 def child(self, name):
71@@ -141,7 +146,7 @@
72 @rtype: C{Deferred} -> C{unicode}
73 @return: The description.
74 """
75- url = self.getURL(endpoint) + '?' + urlencode({'returnDescription': 'true'})
76+ url = self.getURL(endpoint) + '?returnDescription=true'
77 d = endpoint.submit(url=url, method='GET')
78 return d.addCallback(lambda res: res[u'description'])
79
80@@ -179,7 +184,7 @@
81 namespaces.append(self.child(name))
82 return namespaces
83
84- url = self.getURL(endpoint) + '?' + urlencode({'returnNamespaces': 'true'})
85+ url = self.getURL(endpoint) + '?returnNamespaces=true'
86 d = endpoint.submit(url=url, method='GET')
87 return d.addCallback(_parseResponse)
88
89@@ -200,7 +205,7 @@
90 namespaces.append(self.tag(name))
91 return namespaces
92
93- url = self.getURL(endpoint) + '?' + urlencode({'returnTags': 'true'})
94+ url = self.getURL(endpoint) + '?returnTags=true'
95 d = endpoint.submit(url=url, method='GET')
96 return d.addCallback(_parseResponse)
97
98@@ -273,7 +278,7 @@
99 """
100 Representation of a FluidDB tag.
101 """
102- collectionName = 'tags'
103+ collectionName = u'tags'
104
105
106 def getDescription(self, endpoint):
107@@ -286,7 +291,7 @@
108 @rtype: C{Deferred} -> C{unicode}
109 @return: The description.
110 """
111- url = self.getURL(endpoint) + '?' + urlencode({'returnDescription': 'true'})
112+ url = self.getURL(endpoint) + '?returnDescription=true'
113 d = endpoint.submit(url=url, method='GET')
114 return d.addCallback(lambda res: res[u'description'])
115
116@@ -356,12 +361,15 @@
117 return getPage(*a, **kw)
118
119
120- def submit(self, url, method, data=None):
121+ def submit(self, url, method, data=None, headers=None):
122 """
123 Submit a request through this endpoint.
124 """
125- headers = {'Accept': 'application/json'}
126- if data is not None:
127+ if headers is None:
128+ headers = {}
129+
130+ headers['Accept'] = 'application/json'
131+ if data is not None and not isinstance(data, str):
132 data = json.dumps(data)
133 headers['Content-Type'] = 'application/json'
134
135@@ -369,5 +377,202 @@
136 if res:
137 return json.loads(unicode(res, 'utf-8'))
138
139- d = self.getPage(url=url, method=method, postdata=data, headers=headers, agent='txFluidDB')
140+ d = self.getPage(url=url,
141+ method=method,
142+ postdata=data,
143+ headers=headers,
144+ agent='txFluidDB')
145 return d.addCallback(_parse)
146+
147+
148+
149+class Object(_HasPath):
150+ """
151+ A FluidDB object.
152+
153+ @type uuid: C{unicode}
154+ @ivar uuid: The UUID of the object.
155+ """
156+ collectionName = u'objects'
157+
158+
159+ def __init__(self, uuid):
160+ self.uuid = uuid
161+
162+
163+ @property
164+ def components(self):
165+ """
166+ Our only path component is the object UUID.
167+ """
168+ return [self.uuid]
169+
170+
171+ @classmethod
172+ def create(cls, endpoint, about=None):
173+ """
174+ Create a new object.
175+
176+ @type endpoint: L{Endpoint}
177+ @param endpoint: The endpoint to operate through.
178+
179+ @type about: C{unicode} or C{None}
180+ @param about: The value for the about tag, if desired.
181+
182+ @rtype: C{Deferred} -> L{Object}
183+ @return: The newly created object.
184+ """
185+ url = endpoint.getRootURL() + 'objects'
186+ data = {}
187+ if about is not None:
188+ data[u'about'] = about
189+
190+ def _parseResponse(response):
191+ return Object(response[u'id'])
192+
193+ d = endpoint.submit(url=url, method='POST', data=data)
194+ return d.addCallback(_parseResponse)
195+
196+
197+ @classmethod
198+ def query(cls, endpoint, query):
199+ """
200+ Search for objects that match a query.
201+
202+ @type endpoint: L{Endpoint}
203+ @param endpoint: The endpoint to operate through.
204+
205+ @type query: C{unicode}
206+ @param query: A query string to search on.
207+ """
208+ qs = '?' + urlencode({'query': query.encode('utf-8')})
209+ url = endpoint.getRootURL() + 'objects' + qs
210+
211+ def _parseResponse(response):
212+ return [Object(uuid) for uuid in response['ids']]
213+
214+ d = endpoint.submit(url=url, method='GET')
215+ return d.addCallback(_parseResponse)
216+
217+
218+ def getTags(self, endpoint):
219+ """
220+ Get the visible tags on this object.
221+
222+ @type endpoint: L{Endpoint}
223+ @param endpoint: The endpoint to operate through.
224+
225+ @rtype: C{Deferred} -> (C{unicode}, C{list} of L{Tag})
226+ @return: A tuple of the about tag value and the list of tags.
227+ """
228+ def _parseResponse(response):
229+ about = response[u'about']
230+ tags = [Tag(*path.split(u'/')) for path in response[u'tagPaths']]
231+ return (about, tags)
232+
233+ url = self.getURL(endpoint)
234+ d = endpoint.submit(url=url,
235+ method='GET',
236+ data={u'showAbout': True})
237+ return d.addCallback(_parseResponse)
238+
239+
240+ def get(self, endpoint, tag):
241+ """
242+ Get the value of a tag on this object.
243+
244+ @type endpoint: L{Endpoint}
245+ @param endpoint: The endpoint to operate through.
246+
247+ @type tag: L{Tag}
248+ @param tag: The tag to retrieve.
249+
250+ @rtype: Varies depending on what value is stored.
251+ @return: The stored value.
252+ """
253+ def _parseResponse(response):
254+ value = response[u'value']
255+ if u'valueType' in response:
256+ encoding = response[u'valueEncoding']
257+ if encoding != u'base-64':
258+ raise ValueError('Unsupported encoding received: %r' % encoding)
259+ value = value.decode('base-64')
260+ return Blob(response[u'valueType'], value)
261+ return value
262+
263+ url = self.getURL(endpoint, suffix=tag.components) + '?format=json'
264+ d = endpoint.submit(url=url,
265+ method='GET',
266+ headers={'Accept-Encoding': 'base-64'})
267+ return d.addCallback(_parseResponse)
268+
269+
270+ def set(self, endpoint, tag, value):
271+ """
272+ Set the value of a tag on this object.
273+
274+ @type endpoint: L{Endpoint}
275+ @param endpoint: The endpoint to operate through.
276+
277+ @type tag: L{Tag}
278+ @param tag: The tag to set or replace.
279+
280+ @type value: Any JSON-encodable value
281+ @param value: The value to store.
282+ """
283+ url = self.getURL(endpoint, suffix=tag.components) + '?format=json'
284+ return endpoint.submit(url=url,
285+ method='PUT',
286+ data={u'value': value})
287+
288+
289+ def setBlob(self, endpoint, tag, value):
290+ """
291+ Set the value of a tag on this object to a blob.
292+
293+ @type endpoint: L{Endpoint}
294+ @param endpoint: The endpoint to operate through.
295+
296+ @type tag: L{Tag}
297+ @param tag: The tag to set or replace.
298+
299+ @type value: L{Blob}
300+ @param value: The value to store.
301+ """
302+ url = self.getURL(endpoint, suffix=tag.components)
303+ return endpoint.submit(url=url,
304+ method='PUT',
305+ headers={'Content-Type': value.contentType},
306+ data=value.data)
307+
308+
309+ def delete(self, endpoint, tag):
310+ """
311+ Delete a tag from this object.
312+
313+ Note that FluidDB does not support deleting objects themselves.
314+
315+ @type endpoint: L{Endpoint}
316+ @param endpoint: The endpoint to operate through.
317+
318+ @type tag: L{Tag}
319+ @param tag: The tag to retrieve.
320+ """
321+ url = self.getURL(endpoint, suffix=tag.components)
322+ return endpoint.submit(url=url, method='DELETE')
323+
324+
325+
326+class Blob(object):
327+ """
328+ A binary blob with a MIME content type.
329+
330+ @type contentType: C{unicode}
331+ @ivar contentType: The MIME content-type of the data.
332+
333+ @type data: C{str}
334+ @ivar data: The actual data.
335+ """
336+ def __init__(self, contentType, data):
337+ self.contentType = contentType
338+ self.data = data
339
340=== modified file 'txfluiddb/test/test_client.py'
341--- txfluiddb/test/test_client.py 2009-08-19 20:33:58 +0000
342+++ txfluiddb/test/test_client.py 2009-08-21 00:42:26 +0000
343@@ -4,7 +4,7 @@
344 import simplejson as json
345
346 from txfluiddb.errors import InvalidName
347-from txfluiddb.client import Namespace, Tag, Endpoint, _HasPath
348+from txfluiddb.client import Namespace, Tag, Endpoint, _HasPath, Object, Blob
349
350
351
352@@ -44,6 +44,28 @@
353 'http://fluiddb.test.url/tests/foo')
354
355
356+ def test_getURLPrefix(self):
357+ """
358+ Passing prefix to getURL prepends the given components to the path.
359+ """
360+ endpoint = Endpoint('http://fluiddb.test.url/')
361+ path = _HasPath(u'foo')
362+ path.collectionName = 'tests'
363+ self.assertEqual(path.getURL(endpoint, prefix=[u'quux', u'42']),
364+ 'http://fluiddb.test.url/quux/42/foo')
365+
366+
367+ def test_getURLSuffix(self):
368+ """
369+ Passing suffix to getURL appends the given components to the path.
370+ """
371+ endpoint = Endpoint('http://fluiddb.test.url/')
372+ path = _HasPath(u'foo')
373+ path.collectionName = 'tests'
374+ self.assertEqual(path.getURL(endpoint, suffix=[u'quux', u'42']),
375+ 'http://fluiddb.test.url/tests/foo/quux/42')
376+
377+
378 def test_urlEncoding(self):
379 """
380 URL encoding is done according to IRI rules.
381@@ -374,3 +396,199 @@
382 endpoint = Endpoint('http://fluiddb.test.url/', '20090817')
383 self.assertEqual(endpoint.getRootURL(),
384 'http://fluiddb.test.url/20090817/')
385+
386+
387+
388+class ObjectTests(TestCase):
389+ """
390+ Tests for L{Object}.
391+ """
392+ def setUp(self):
393+ self.endpoint = MockEndpoint('http://fluiddb.test.url/')
394+
395+
396+ def test_getTags(self):
397+ """
398+ The getTags method returns the about value and the list of tags.
399+ """
400+ response = {u'about': u'Testing object',
401+ u'tagPaths': [u'test/tag1', u'test/tag2']}
402+ self.endpoint.response = json.dumps(response)
403+
404+ def _gotResponse(response):
405+ about, tags = response
406+ self.assertEqual(about, u'Testing object')
407+ self.assertEqual(len(tags), 2)
408+ tag1, tag2 = tags
409+ self.assertIsInstance(tag1, Tag)
410+ self.assertEqual(tag1.getPath(), u'test/tag1')
411+ self.assertIsInstance(tag2, Tag)
412+ self.assertEqual(tag2.getPath(), u'test/tag2')
413+
414+ obj = Object(u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
415+ d = obj.getTags(self.endpoint).addCallback(_gotResponse)
416+ self.assertEqual(self.endpoint.method, 'GET')
417+ self.assertEqual(
418+ self.endpoint.url,
419+ 'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
420+ self.assertEqual(json.loads(self.endpoint.data),
421+ {u'showAbout': True})
422+ return d
423+
424+
425+ def makeGetRequest(self, response):
426+ """
427+ Utility wrapper for testing L{Object.get} operations.
428+ """
429+ self.endpoint.response = json.dumps(response)
430+ obj = Object(u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
431+ tag = Namespace(u'test').child(u'tag')
432+ d = obj.get(self.endpoint, tag)
433+ self.assertEqual(self.endpoint.method, 'GET')
434+ self.assertEqual(self.endpoint.data, None)
435+ self.assertEqual(
436+ self.endpoint.url,
437+ 'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx/test/tag?format=json')
438+ return d
439+
440+
441+ def test_getString(self):
442+ """
443+ Retrieving a JSON string value results in a C{unicode} object.
444+ """
445+ response = {u'value': u'Data goes where?'}
446+ def _gotResponse(response):
447+ self.assertIsInstance(response, unicode)
448+ self.assertEqual(response, u'Data goes where?')
449+ return self.makeGetRequest(response).addCallback(_gotResponse)
450+
451+
452+ def test_getInteger(self):
453+ """
454+ Retrieving a JSON integer value results in an C{integer} object.
455+ """
456+ response = {u'value': 42}
457+ def _gotResponse(response):
458+ self.assertEqual(response, 42)
459+ return self.makeGetRequest(response).addCallback(_gotResponse)
460+
461+
462+ def test_getBlob(self):
463+ """
464+ Retrieving a binary value results in a C{Blob} object.
465+ """
466+ response = {u'value': u'PHA+Zm9vPC9wPg==',
467+ u'valueEncoding': u'base-64',
468+ u'valueType': u'text/html'}
469+ def _gotResponse(response):
470+ self.assertEqual(self.endpoint.headers['Accept-Encoding'], 'base-64')
471+ self.assertEqual(response.contentType, u'text/html')
472+ self.assertEqual(response.data, '<p>foo</p>')
473+ return self.makeGetRequest(response).addCallback(_gotResponse)
474+
475+
476+ def test_deleteTag(self):
477+ """
478+ Deleting a tag returns nothing if successful.
479+ """
480+ self.endpoint.response = ''
481+
482+ obj = Object(u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
483+ tag = Namespace(u'test').child(u'tag')
484+ d = obj.delete(self.endpoint, tag)
485+ self.assertEqual(self.endpoint.method, 'DELETE')
486+ self.assertEqual(
487+ self.endpoint.url,
488+ 'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx/test/tag')
489+ self.assertEqual(self.endpoint.data, None)
490+ return d
491+
492+
493+ def test_setTag(self):
494+ self.endpoint.response = ''
495+
496+ obj = Object(u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
497+ tag = Namespace(u'test').child(u'tag')
498+ d = obj.set(self.endpoint, tag, 42)
499+ self.assertEqual(self.endpoint.method, 'PUT')
500+ self.assertEqual(
501+ self.endpoint.url,
502+ 'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx/test/tag?format=json')
503+ self.assertEqual(json.loads(self.endpoint.data),
504+ {u'value': 42})
505+ return d
506+
507+
508+ def test_setBlob(self):
509+ self.endpoint.response = ''
510+
511+ obj = Object(u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
512+ tag = Namespace(u'test').child(u'tag')
513+ blob = Blob(u'text/html', '<p>foo</p>')
514+ d = obj.setBlob(self.endpoint, tag, blob)
515+ self.assertEqual(self.endpoint.method, 'PUT')
516+ self.assertEqual(
517+ self.endpoint.url,
518+ 'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx/test/tag')
519+ self.assertEqual(self.endpoint.data, '<p>foo</p>')
520+ self.assertEqual(self.endpoint.headers['Content-Type'], 'text/html')
521+ return d
522+
523+
524+ def test_createObject(self):
525+ """
526+ Creating an object returns the newly created object.
527+ """
528+ response = {u'URI': u'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx',
529+ u'id': u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'}
530+ self.endpoint.response = json.dumps(response)
531+
532+ def _gotResponse(obj):
533+ self.assertIsInstance(obj, Object)
534+ self.assertEqual(obj.uuid, u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
535+
536+ d = Object.create(self.endpoint).addCallback(_gotResponse)
537+ self.assertEqual(self.endpoint.method, 'POST')
538+ self.assertEqual(self.endpoint.url, 'http://fluiddb.test.url/objects')
539+ self.assertEqual(json.loads(self.endpoint.data), {})
540+ return d
541+
542+
543+ def test_createObjectWithAbout(self):
544+ response = {u'URI': u'http://fluiddb.test.url/objects/xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx',
545+ u'id': u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'}
546+ self.endpoint.response = json.dumps(response)
547+
548+ def _gotResponse(obj):
549+ self.assertIsInstance(obj, Object)
550+ self.assertEqual(obj.uuid, u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
551+
552+ d = Object.create(self.endpoint, u'New object').addCallback(_gotResponse)
553+ self.assertEqual(self.endpoint.method, 'POST')
554+ self.assertEqual(self.endpoint.url, 'http://fluiddb.test.url/objects')
555+ self.assertEqual(json.loads(self.endpoint.data),
556+ {u'about': u'New object'})
557+ return d
558+
559+
560+ def test_query(self):
561+ """
562+ Querying returns all objects matching the query.
563+ """
564+ response = {u'ids': [u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxx1',
565+ u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxx2']}
566+ self.endpoint.response = json.dumps(response)
567+
568+ def _gotResponse(objs):
569+ self.assertEqual(len(objs), 2)
570+ obj1, obj2 = objs
571+ self.assertIsInstance(obj1, Object)
572+ self.assertEqual(obj1.uuid, u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxx1')
573+ self.assertIsInstance(obj2, Object)
574+ self.assertEqual(obj2.uuid, u'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxx2')
575+
576+ d = Object.query(self.endpoint, u'has fluiddb/about').addCallback(_gotResponse)
577+ self.assertEqual(self.endpoint.method, 'GET')
578+ self.assertEqual(self.endpoint.url, 'http://fluiddb.test.url/objects?query=has+fluiddb%2Fabout')
579+ self.assertEqual(self.endpoint.data, None)
580+ return d

Subscribers

People subscribed via source and target branches