Merge lp:~soren/surveilr/extend-client into lp:surveilr

Proposed by Soren Hansen
Status: Merged
Approved by: Soren Hansen
Approved revision: 29
Merged at revision: 29
Proposed branch: lp:~soren/surveilr/extend-client
Merge into: lp:surveilr
Diff against target: 701 lines (+448/-98)
4 files modified
surveilr/admin.py (+55/-3)
surveilr/api/client.py (+108/-24)
surveilr/tests/api/test_client.py (+204/-52)
surveilr/tests/test_admin.py (+81/-19)
To merge this branch: bzr merge lp:~soren/surveilr/extend-client
Reviewer Review Type Date Requested Status
Soren Hansen Pending
Review via email: mp+90631@code.launchpad.net

Commit message

Extend client to deal with Services and Metrics, and expose get() and delete() methods.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'surveilr/admin.py'
2--- surveilr/admin.py 2012-01-28 21:09:53 +0000
3+++ surveilr/admin.py 2012-01-29 23:36:23 +0000
4@@ -96,14 +96,63 @@
5 print '--user %s --api_key %s' % (user.user_id, user.key)
6
7
8+class ShowUser(Command):
9+ def __call__(self):
10+ user = self.client.get_user(self.args[0])
11+
12+ print 'User:'
13+ print 'ID:', user.user_id
14+ print 'Admin:', user.admin
15+
16+
17+class DeleteUser(Command):
18+ def __call__(self):
19+ self.client.delete_user(self.args[0])
20+ print 'User deleted'
21+
22+
23+class ShowService(Command):
24+ def __call__(self):
25+ service = self.client.get_service(self.args[0])
26+
27+ print 'Service:'
28+ print 'ID:', service.id
29+ print 'Name:', service.name
30+ print 'Plugins:', service.plugins
31+
32+
33+class DeleteService(Command):
34+ def __call__(self):
35+ self.client.delete_service(self.args[0])
36+ print 'Service deleted'
37+
38+
39+class CreateService(Command):
40+ def get_optparser(self):
41+ optparser = super(CreateService, self).get_optparser()
42+ optparser.add_option('-p', action='append', dest='plugins',
43+ help='Make the new user an admin')
44+ return optparser
45+
46+ def __call__(self):
47+ service = self.client.new_service(self.args[0],
48+ self.options.plugins)
49+ print 'Service:'
50+ print 'ID:', service.id
51+
52+
53+def usage():
54+ for cmd_name in commands:
55+ cmd = commands[cmd_name]()
56+ print cmd.optparser.get_usage().strip()
57+
58+
59 def main(argv=None):
60 if argv is None: # pragma: nocover
61 argv = sys.argv[1:]
62
63 if not len(argv):
64- for cmd_name in commands:
65- cmd = commands[cmd_name]()
66- print cmd.optparser.get_usage().strip()
67+ usage()
68 return False
69
70 if argv[0] in commands:
71@@ -114,6 +163,9 @@
72 except surveilr.api.client.UnauthorizedError:
73 print 'Action not permitted'
74 return False
75+ else:
76+ usage()
77+ return False
78
79
80 if __name__ == '__main__': # pragma: nocover
81
82=== modified file 'surveilr/api/client.py'
83--- surveilr/api/client.py 2012-01-28 21:09:53 +0000
84+++ surveilr/api/client.py 2012-01-29 23:36:23 +0000
85@@ -34,8 +34,22 @@
86 self.auth = auth
87
88 def new_user(self, *args, **kwargs):
89- user = User.new(self, *args, **kwargs)
90- return user
91+ return User.new(self, *args, **kwargs)
92+
93+ def get_user(self, *args, **kwargs):
94+ return User.get(self, *args, **kwargs)
95+
96+ def delete_user(self, *args, **kwargs):
97+ return User.delete(self, *args, **kwargs)
98+
99+ def new_service(self, *args, **kwargs):
100+ return Service.new(self, *args, **kwargs)
101+
102+ def get_service(self, *args, **kwargs):
103+ return Service.get(self, *args, **kwargs)
104+
105+ def delete_service(self, *args, **kwargs):
106+ return Service.delete(self, *args, **kwargs)
107
108 def send_req(self, url_tail, method, body):
109 http = httplib2.Http()
110@@ -49,13 +63,6 @@
111 raise UnauthorizedError(resp.reason)
112 return contents
113
114- def req(self, obj_type, action, data):
115- if action == 'create':
116- method = 'POST'
117- url_tail = '/%ss' % (obj_type.url_part)
118-
119- return self.send_req(url_tail, method=method, body=json.dumps(data))
120-
121
122 class SurveilrDirectClient(SurveilrClient):
123 def __init__(self, *args, **kwargs):
124@@ -73,27 +80,104 @@
125
126
127 class APIObject(object):
128- def __init__(self, client):
129+ keys = []
130+
131+ def __init__(self, client, **kwargs):
132 self.client = client
133-
134- def req(self, action, data):
135- return self.client.req(type(self), action, data)
136+ for key in self.keys:
137+ setattr(self, key, kwargs[key])
138+
139+ @classmethod
140+ def req(cls, client, action, data, parent=None):
141+ if action == 'create':
142+ kwargs = {'method': 'POST', 'body': json.dumps(data)}
143+ url_tail = '/%ss' % (cls.url_part)
144+ elif action == 'get':
145+ kwargs = {'method': 'GET'}
146+ url_tail = '/%ss/%s' % (cls.url_part, data)
147+ elif action == 'delete':
148+ kwargs = {'method': 'DELETE'}
149+ url_tail = '/%ss/%s' % (cls.url_part, data)
150+
151+ if parent is not None:
152+ url_tail = parent.instance_url_tail() + url_tail
153+
154+ return client.send_req(url_tail, **kwargs)
155+
156+ def instance_url_tail(self):
157+ return '/%ss/%s' % (self.url_part, self.id)
158+
159+ @classmethod
160+ def delete(cls, client, id):
161+ return cls.req(client, 'delete', id)
162+
163+ @classmethod
164+ def get(cls, client, id):
165+ return cls.json_deserialise(client, cls.req(client, 'get', id))
166+
167+ def __repr__(self):
168+ return ('<%s object%s>' %
169+ (self.__class__.__name__,
170+ ''.join([', %s=%r' %
171+ (key, getattr(self, key)) for key in self.keys])))
172
173
174 class User(APIObject):
175 url_part = 'user'
176+ keys = ['id', 'key', 'admin']
177
178- def __init__(self, client, obj_data=None):
179- self.client = client
180- obj_data = json.loads(obj_data)
181- self.user_id = obj_data['id']
182- self.key = obj_data['key']
183- self.admin = obj_data['admin']
184+ @classmethod
185+ def json_deserialise(cls, client, s):
186+ d = json.loads(s)
187+ return cls(client, id=d.get('id'),
188+ key=d.get('key'),
189+ admin=d.get('admin', False))
190
191 @classmethod
192 def new(cls, client, admin=False):
193- return cls(client, client.req(cls, 'create', {'admin': admin}))
194-
195- def __repr__(self):
196- return ('<User object, user_id=%r, key=%r, admin=%r>' %
197- (self.user_id, self.key, self.admin))
198+ return cls.json_deserialise(client,
199+ cls.req(client,
200+ 'create',
201+ {'admin': admin}))
202+
203+
204+class Service(APIObject):
205+ url_part = 'service'
206+ keys = ['id', 'name', 'plugins']
207+
208+ @classmethod
209+ def json_deserialise(cls, client, s):
210+ d = json.loads(s)
211+ return cls(client, id=d.get('id'),
212+ name=d.get('name'),
213+ plugins=d.get('plugins', []))
214+
215+ @classmethod
216+ def new(cls, client, name, plugins=None):
217+ return cls.json_deserialise(client,
218+ cls.req(client, 'create',
219+ {'name': name,
220+ 'plugins': plugins}))
221+
222+ def new_metric(self, *args, **kwargs):
223+ return Metric.new(self.client, self, *args, **kwargs)
224+
225+
226+class Metric(APIObject):
227+ url_part = 'metric'
228+ keys = ['id', 'service', 'timestamp', 'metrics']
229+
230+ @classmethod
231+ def json_deserialise(cls, client, s):
232+ d = json.loads(s)
233+ return cls(client, id=d.get('id'),
234+ service=d.get('service'),
235+ timestamp=d.get('timestamp'),
236+ metrics=d.get('metrics'))
237+
238+ @classmethod
239+ def new(cls, client, service, metrics):
240+ return cls.json_deserialise(client,
241+ cls.req(client, 'create',
242+ {'metrics': metrics},
243+ parent=service))
244
245=== modified file 'surveilr/tests/api/test_client.py'
246--- surveilr/tests/api/test_client.py 2012-01-28 21:09:53 +0000
247+++ surveilr/tests/api/test_client.py 2012-01-29 23:36:23 +0000
248@@ -5,7 +5,7 @@
249 from surveilr.api import client
250
251
252-class APIClientTests(tests.TestCase):
253+class SurveilrClientTests(tests.TestCase):
254 test_url = 'http://somewhere:1234/else'
255 test_auth = ('foo', 'bar')
256
257@@ -16,19 +16,31 @@
258 auth = self.test_auth
259 return client.SurveilrClient(self.test_url, self.test_auth)
260
261- @mock.patch('surveilr.api.client.User')
262- def test_new_user(self, User):
263- test_args = ('foo', 'bar', 'baz')
264- test_kwargs = {'foo': 'bar',
265- 'baz': 'wibble'}
266-
267- api_client = self._get_client()
268- user = api_client.new_user(*test_args, **test_kwargs)
269- User.new.assert_called_with(api_client, *test_args, **test_kwargs)
270- self.assertEquals(user, User.new.return_value)
271+ def test_client_class_actions(self):
272+ """client.{new,get,delete}_{user,service} proxied to class methods"""
273+
274+ for op in ['delete', 'new', 'get']:
275+ for t in ['User', 'Service']:
276+ @mock.patch('surveilr.api.client.%s' % t)
277+ def test_action(cls):
278+ test_args = ('foo', 'bar', 'baz')
279+ test_kwargs = {'foo': 'bar',
280+ 'baz': 'wibble'}
281+ api_client = self._get_client()
282+ client_obj_func = getattr(api_client,
283+ '%s_%s' % (op, t.lower()))
284+
285+ user = client_obj_func(*test_args, **test_kwargs)
286+
287+ cls_func = getattr(cls, op)
288+ cls_func.assert_called_with(api_client, *test_args,
289+ **test_kwargs)
290+ self.assertEquals(user, cls_func.return_value)
291+ test_action()
292
293 @mock.patch('surveilr.api.client.httplib2')
294 def test_send_req(self, httplib2):
295+ # Setup
296 api_client = self._get_client()
297
298 class FakeResponse(object):
299@@ -37,16 +49,24 @@
300
301 http = httplib2.Http.return_value
302 http.request.return_value = (FakeResponse(200), 'resp')
303+
304+ # Exercise
305 client_response = api_client.send_req('tail', 'METHOD', 'body')
306+
307+ # Verify that the credentials are passed
308 http.add_credentials.assert_called_with(*self.test_auth)
309+ # Verify that the given method is used, the body is passed correctly,
310+ # and that the url is constructed correctlyi
311 http.request.assert_called_with(self.test_url + '/tail',
312 method='METHOD',
313 body='body')
314
315+ # Verify that the response comes back intact
316 self.assertEquals(client_response, 'resp')
317
318 @mock.patch('surveilr.api.client.httplib2')
319 def test_send_req_403_reraises_unauthorized_error(self, httplib2):
320+ # Setup
321 api_client = self._get_client()
322
323 class FakeResponse(object):
324@@ -57,58 +77,190 @@
325
326 http = httplib2.Http.return_value
327 http.request.return_value = (FakeResponse(403), 'resp')
328+
329+ # Exercise and verify
330 self.assertRaises(client.UnauthorizedError,
331 api_client.send_req, 'tail', 'METHOD', 'body')
332
333- def test_req(self):
334- api_client = self._get_client()
335-
336- test_data = {'foo': ['bar', 'baz', 2]}
337-
338- class FakeObjType(object):
339- url_part = 'stuff'
340-
341- with mock.patch_object(api_client, 'send_req') as send_req:
342- api_client.req(FakeObjType(), 'create', test_data)
343+ def test_req_create_with_parend(self):
344+ api_client = self._get_client()
345+
346+ test_data = {'foo': ['bar', 'baz', 2]}
347+
348+ class FakeObjType(client.APIObject):
349+ url_part = 'stuff'
350+
351+ parent = FakeObjType(api_client)
352+ parent.id = 7
353+ with mock.patch_object(api_client, 'send_req') as send_req:
354+ FakeObjType(api_client).req(api_client, 'create', test_data,
355+ parent=parent)
356+ self.assertEquals(send_req.call_args[0][0], '/stuffs/7/stuffs')
357+ self.assertEquals(send_req.call_args[1]['method'], 'POST')
358+ self.assertEquals(json.loads(send_req.call_args[1]['body']),
359+ test_data)
360+
361+ def test_req_create(self):
362+ api_client = self._get_client()
363+
364+ test_data = {'foo': ['bar', 'baz', 2]}
365+
366+ class FakeObjType(client.APIObject):
367+ url_part = 'stuff'
368+
369+ with mock.patch_object(api_client, 'send_req') as send_req:
370+ FakeObjType(api_client).req(api_client, 'create', test_data)
371 self.assertEquals(send_req.call_args[0][0], '/stuffs')
372 self.assertEquals(send_req.call_args[1]['method'], 'POST')
373 self.assertEquals(json.loads(send_req.call_args[1]['body']),
374 test_data)
375
376- def test_apiobject(self):
377+ def test_req_get(self):
378+ api_client = self._get_client()
379+
380+ class FakeObjType(client.APIObject):
381+ url_part = 'stuff'
382+
383+ with mock.patch_object(api_client, 'send_req') as send_req:
384+ FakeObjType(api_client).req(api_client, 'get', 'someid')
385+ self.assertEquals(send_req.call_args[0][0], '/stuffs/someid')
386+ self.assertEquals(send_req.call_args[1]['method'], 'GET')
387+
388+ def test_req_delete(self):
389+ api_client = self._get_client()
390+
391+ class FakeObjType(client.APIObject):
392+ url_part = 'stuff'
393+
394+ with mock.patch_object(api_client, 'send_req') as send_req:
395+ FakeObjType(api_client).req(api_client, 'delete', 'someid')
396+ self.assertEquals(send_req.call_args[0][0], '/stuffs/someid')
397+ self.assertEquals(send_req.call_args[1]['method'], 'DELETE')
398+
399+
400+class APIObjectTests(tests.TestCase):
401+ def _test_req(self):
402 client_obj = mock.Mock()
403 data = mock.Sentinel()
404
405 class TestObject(client.APIObject):
406- pass
407+ url_part = 'test'
408
409 obj = TestObject(client_obj)
410 obj.req('ACTION', data)
411
412 client_obj.req.assert_called_with(TestObject, 'ACTION', data)
413
414-
415-class APITypeTests(tests.TestCase):
416- def _test_user_new(self, admin):
417- client_obj = mock.Mock()
418- client_obj.req.return_value = json.dumps({'id': 'testid',
419+ def _test_get_or_delete(self, op, req_return=None):
420+ client_obj = mock.Mock()
421+
422+ class TestObject(client.APIObject):
423+ url_part = 'test'
424+
425+ @classmethod
426+ def json_deserialise(inner_self, client, s):
427+ self.assertEquals(s, 'lots of json')
428+
429+ req = mock.Mock()
430+
431+ TestObject.req.return_value = req_return
432+
433+ getattr(TestObject, op)(client_obj, 'testid')
434+ TestObject.req.assert_called_with(client_obj, op, 'testid')
435+
436+ def test_delete(self):
437+ self._test_get_or_delete('delete')
438+
439+ def test_get(self):
440+ self._test_get_or_delete('get', 'lots of json')
441+
442+
443+class UserTests(tests.TestCase):
444+ def _test_new(self, admin):
445+ # Setup
446+ client_obj = mock.Mock()
447+ with mock.patch_object(client.User, 'req') as client_req:
448+ client_req.return_value = json.dumps({'id': 'testid',
449 'key': 'testkey',
450 'admin': admin})
451- user = client.User.new(client_obj)
452- client_obj.req.assert_called_with(client.User, 'create',
453+
454+ # Exercise
455+ user = client.User.new(client_obj)
456+
457+ # Verify
458+ client_req.assert_called_with(client_obj, 'create',
459 {'admin': False})
460
461- self.assertEquals(user.user_id, 'testid')
462- self.assertEquals(user.key, 'testkey')
463- self.assertEquals(user.admin, admin)
464- self.assertEquals(repr(user), "<User object, user_id=u'testid', "
465- "key=u'testkey', admin=%r>" % user.admin)
466+ self.assertEquals(user.id, 'testid')
467+ self.assertEquals(user.key, 'testkey')
468+ self.assertEquals(user.admin, admin)
469+ self.assertEquals(repr(user), "<User object, id=u'testid', "
470+ "key=u'testkey', admin=%r>" %
471+ user.admin)
472
473 def test_user_new_admin(self):
474- self._test_user_new(True)
475+ self._test_new(True)
476
477 def test_user_new_not_admin(self):
478- self._test_user_new(False)
479+ self._test_new(False)
480+
481+
482+class ServiceTests(tests.TestCase):
483+ def test_new(self):
484+ # Setup
485+ test_plugins = ['http://h:1/p']
486+ client_obj = mock.Mock()
487+ with mock.patch_object(client.Service, 'req') as service_req:
488+ service_req.return_value = json.dumps({'id': 'testid',
489+ 'name': 'testname',
490+ 'plugins': test_plugins})
491+
492+ # Exercise
493+ service = client.Service.new(client_obj, 'testname',
494+ plugins=test_plugins)
495+
496+ # Verify
497+ service_req.assert_called_with(client_obj, 'create',
498+ {'name': 'testname',
499+ 'plugins': test_plugins})
500+
501+ self.assertEquals(service.id, 'testid')
502+ self.assertEquals(service.name, 'testname')
503+ self.assertEquals(service.plugins, test_plugins)
504+ self.assertEquals(repr(service), "<Service object, id=u'testid', "
505+ "name=u'testname', "
506+ "plugins=[u'http://h:1/p']>")
507+
508+
509+class MetricTests(tests.TestCase):
510+ def test_service_metric_new(self):
511+ # Setup
512+ client_obj = mock.Mock()
513+ service_obj = client.Service(client_obj, id='testid',
514+ name='testname', plugins=[])
515+
516+ with mock.patch_object(client, 'Metric') as Metric:
517+ service_obj.new_metric({'foo': 1234})
518+
519+ Metric.new.assert_called_with(client_obj, service_obj,
520+ {'foo': 1234})
521+
522+
523+ def test_new(self):
524+ # Setup
525+ client_obj = mock.Mock()
526+ service_obj = mock.Sentinel()
527+ with mock.patch_object(client.Metric, 'req') as metric_req:
528+ metric_req.return_value = json.dumps({})
529+
530+ # Exercise
531+ client.Metric.new(client_obj, service_obj, {'something': 1234})
532+
533+ # Verify
534+ metric_req.assert_called_with(client_obj,
535+ 'create',
536+ {'metrics': {'something': 1234}},
537+ parent=service_obj)
538
539
540 class DirectClientTests(tests.TestCase):
541@@ -122,18 +274,18 @@
542 from webob import Request
543
544 api_client = client.SurveilrDirectClient({})
545- api_client.app = mock.Mock()
546- response = mock.Mock()
547- response.body = 'response'
548- api_client.app.return_value = response
549-
550- client_response = api_client.send_req('tail', 'METHOD', 'body')
551-
552- args = api_client.app.call_args[0]
553- self.assertEquals(type(args[0]), Request)
554-
555- req = args[0]
556- self.assertEquals(req.path, 'tail')
557- self.assertEquals(req.method, 'METHOD')
558- self.assertEquals(req.body, 'body')
559- self.assertEquals(client_response, 'response')
560+ with mock.patch_object(api_client, 'app') as app:
561+ response = mock.Mock()
562+ response.body = 'response'
563+ app.return_value = response
564+
565+ client_response = api_client.send_req('tail', 'METHOD', 'body')
566+
567+ args = app.call_args[0]
568+ self.assertEquals(type(args[0]), Request)
569+
570+ req = args[0]
571+ self.assertEquals(req.path, 'tail')
572+ self.assertEquals(req.method, 'METHOD')
573+ self.assertEquals(req.body, 'body')
574+ self.assertEquals(client_response, 'response')
575
576=== modified file 'surveilr/tests/test_admin.py'
577--- surveilr/tests/test_admin.py 2012-01-28 21:09:53 +0000
578+++ surveilr/tests/test_admin.py 2012-01-29 23:36:23 +0000
579@@ -126,24 +126,6 @@
580
581 self.assertIsNone(auth)
582
583- def test_CreateUser_admin(self):
584- create_user = admin.CreateUser()
585-
586- create_user.init(['--admin'])
587- create_user.client = mock.Mock()
588- create_user()
589-
590- create_user.client.new_user.assert_called_with(admin=True)
591-
592- def test_CreateUser_non_admin(self):
593- create_user = admin.CreateUser()
594-
595- create_user.init([])
596- create_user.client = mock.Mock()
597- create_user()
598-
599- create_user.client.new_user.assert_called_with(admin=False)
600-
601 def _replace_stdout_with_stringio(self):
602 saved_stdout = sys.stdout
603
604@@ -163,12 +145,23 @@
605 self.assertEquals(sys.stdout.getvalue(),
606 'Action not permitted\n')
607
608- def test_cmd_list(self):
609+ def test_cmd_list_if_no_command_given(self):
610 self._replace_stdout_with_stringio()
611 ret = admin.main([])
612
613 self.assertEquals(ret, False)
614 stdout = sys.stdout.getvalue()
615+ self._check_cmd_list(stdout)
616+
617+ def test_cmd_list_if_invalid_command_given(self):
618+ self._replace_stdout_with_stringio()
619+ ret = admin.main(['this is not a valid command'])
620+
621+ self.assertEquals(ret, False)
622+ stdout = sys.stdout.getvalue()
623+ self._check_cmd_list(stdout)
624+
625+ def _check_cmd_list(self, stdout):
626 self.assertEquals(len(stdout.split('\n')) - 1, len(admin.commands))
627
628 expected_commands = set(admin.commands.keys())
629@@ -180,3 +173,72 @@
630 found_commands.add(cmd)
631
632 self.assertEquals(expected_commands, found_commands)
633+
634+ def test_CreateUser_admin(self):
635+ create_user = admin.CreateUser()
636+
637+ create_user.init(['--admin'])
638+ create_user.client = mock.Mock()
639+ create_user()
640+
641+ create_user.client.new_user.assert_called_with(admin=True)
642+
643+ def test_CreateUser_non_admin(self):
644+ create_user = admin.CreateUser()
645+
646+ create_user.init([])
647+ create_user.client = mock.Mock()
648+ create_user()
649+
650+ create_user.client.new_user.assert_called_with(admin=False)
651+
652+ def test_CreateService_no_plugins(self):
653+ create_service = admin.CreateService()
654+
655+ create_service.init(['servicename'])
656+ create_service.client = mock.Mock()
657+ create_service()
658+
659+ create_service.client.new_service.assert_called_with('servicename',
660+ None)
661+
662+ def test_CreateService_with_plugins(self):
663+ create_service = admin.CreateService()
664+
665+ plugins = ['http://foo/bar', 'http://baz/wibble']
666+
667+ args = ['servicename']
668+ for plugin in plugins:
669+ args += ['-p', plugin]
670+
671+ create_service.init(args)
672+ create_service.client = mock.Mock()
673+ create_service()
674+
675+ create_service.client.new_service.assert_called_with('servicename',
676+ plugins)
677+
678+ def test_ShowUser(self):
679+ self._test_Show_or_Delete('show', 'user')
680+
681+ def test_ShowService(self):
682+ self._test_Show_or_Delete('show', 'service')
683+
684+ def test_DeleteUser(self):
685+ self._test_Show_or_Delete('delete', 'user')
686+
687+ def test_DeleteService(self):
688+ self._test_Show_or_Delete('delete', 'service')
689+
690+ def _test_Show_or_Delete(self, show_or_delete, type_name):
691+ cmd = getattr(admin, '%s%s' % (show_or_delete.capitalize(),
692+ type_name.capitalize()))()
693+
694+ cmd.init(['testid'])
695+ cmd.client = mock.Mock()
696+ cmd()
697+
698+ method_prefix = show_or_delete == 'show' and 'get' or 'delete'
699+ method_name = '%s_%s' % (method_prefix, type_name)
700+ method = getattr(cmd.client, method_name)
701+ method.assert_called_with('testid')

Subscribers

People subscribed via source and target branches

to all changes: