From efc2d1a675150d84dca99119bc135df107225724 Mon Sep 17 00:00:00 2001 From: Steve Howell Date: Sat, 17 Dec 2016 12:19:15 -0800 Subject: [PATCH] Simplify, document, and fix the API code. We used to create endpoints with Client._register. Now we now have explicit methods for the endpoints. This allows us to add docstrings and stricter mypy annotations. This fix also introduces a call_endpoint() method that avoids the need for manually building urls with API_VERSTRING when you know the URL pattern of the endpoint you want to hit (and when the API doesn't have a convenient wrapper). I fixed a bug with create_users where it now uses PUT instead of POST. I also removed client.export(), which was just broken. I had to change recent-messages and zulip-export, which were using client.do_api_query and Client._register. Now it's easier to just call client.call_endpoint() for situations where our API doesn't have convenient wrappers, so that's what I did with those scripts. --- api/examples/recent-messages | 11 +- api/zulip/__init__.py | 251 ++++++++++++++++++++++++-------- tools/zulip-export/zulip-export | 22 ++- 3 files changed, 213 insertions(+), 71 deletions(-) diff --git a/api/examples/recent-messages b/api/examples/recent-messages index c1e15c9eaa..e1da7a4e00 100755 --- a/api/examples/recent-messages +++ b/api/examples/recent-messages @@ -35,7 +35,7 @@ Example: recent-messages --count=101 --user=username@example.com --api-key=a0b1c You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc """ -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) import zulip parser = optparse.OptionParser(usage=usage) @@ -45,7 +45,7 @@ parser.add_option_group(zulip.generate_option_group(parser)) client = zulip.init_from_options(options) -req = { +request = { 'narrow': [["stream", "Denmark"]], 'num_before': options.count, 'num_after': 0, @@ -53,7 +53,12 @@ req = { 'apply_markdown': False } -old_messages = client.do_api_query(req, zulip.API_VERSTRING + 'messages', method='GET') +old_messages = client.call_endpoint( + url='messages', + method='GET', + request=request, +) + if 'messages' in old_messages: for message in old_messages['messages']: print(json.dumps(message, indent=4)) diff --git a/api/zulip/__init__.py b/api/zulip/__init__.py index 43f38f0029..054948f85f 100644 --- a/api/zulip/__init__.py +++ b/api/zulip/__init__.py @@ -38,7 +38,7 @@ from six.moves.configparser import SafeConfigParser from six.moves import urllib import logging import six -from typing import Any, Callable, Dict, Mapping, Optional, Tuple, Union +from typing import Any, Callable, Dict, Mapping, Optional, Tuple, Union, Iterable __version__ = "0.2.5" @@ -290,7 +290,7 @@ class Client(object): vendor_version=vendor_version, ) - def do_api_query(self, orig_request, url, method="POST", longpolling = False): + def do_api_query(self, orig_request, url, method="POST", longpolling=False): # type: (Mapping[str, Any], str, str, bool) -> Dict[str, Any] request = {} @@ -402,26 +402,11 @@ class Client(object): return {'msg': "Unexpected error from the server", "result": "http-error", "status_code": res.status_code} - @classmethod - def _register(cls, name, url=None, make_request=None, - method="POST", computed_url=None, **query_kwargs): - # type: (Any, str, Optional[Callable], str, Optional[Callable], **Any) -> Any - if url is None: - url = name - if make_request is None: - def make_request(**kwargs): - # type: (**Any) -> Any - return kwargs.get("request", {}) - def call(self, *args, **kwargs): - # type: (*Any, **Any) -> str - request = make_request(*args, **kwargs) - if computed_url is not None: - req_url = computed_url(request) - else: - req_url = url - return self.do_api_query(request, API_VERSTRING + req_url, method=method, **query_kwargs) - call.__name__ = name - setattr(cls, name, call) + def call_endpoint(self, url=None, method="POST", request=None, longpolling=False): + # type: (str, str, Dict[str, Any], bool) -> Dict[str, Any] + if request is None: + request = dict() + return self.do_api_query(request, API_VERSTRING + url, method=method) def call_on_each_event(self, callback, event_types=None, narrow=None): # type: (Callable, Optional[List[str]], Any) -> None @@ -485,31 +470,193 @@ class Client(object): callback(event['message']) self.call_on_each_event(event_callback, ['message']) -def _mk_subs(streams, **kwargs): - # type: (str, **Any ) -> Dict[str, str] - result = kwargs - result['subscriptions'] = streams - return result + def send_message(self, message_data): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + See api/examples/send-message for example usage. + ''' + return self.call_endpoint( + url='messages', + request=message_data, + ) -def _mk_rm_subs(streams): - # type: (str) -> Dict[str, str] - return {'delete': streams} + def update_message(self, message_data): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + See api/examples/edit-message for example usage. + ''' + return self.call_endpoint( + url='messages', + method='PATCH', + request=message_data, + ) -def _mk_deregister(queue_id): - # type: (str) -> Dict[str, str] - return {'queue_id': queue_id} + def get_events(self, **request): + # type: (**Any) -> Dict[str, Any] + ''' + See the register() method for example usage. + ''' + return self.call_endpoint( + url='events', + method='GET', + longpolling=True, + request=request, + ) -def _mk_events(event_types=None, narrow=None): - # type: (Any, List[None]) -> Dict[Any, List[None]] - if event_types is None: - return dict() - if narrow is None: - narrow = [] - return dict(event_types=event_types, narrow=narrow) + def register(self, event_types=None, narrow=None, **kwargs): + # type: (Iterable[str], Any, **Any) -> Dict[str, Any] + ''' + Example usage: -def _kwargs_to_dict(**kwargs): - # type: (**Any) -> Any - return kwargs + >>> client.register(['message']) + {u'msg': u'', u'max_message_id': 112, u'last_event_id': -1, u'result': u'success', u'queue_id': u'1482093786:2'} + >>> client.get_events(queue_id='1482093786:2', last_event_id=0) + {...} + ''' + + if narrow is None: + narrow = [] + + request = dict( + event_types=event_types, + narrow=narrow, + **kwargs + ) + + return self.call_endpoint( + url='register', + request=request, + ) + + def deregister(self, queue_id): + # type: (str) -> Dict[str, Any] + ''' + Example usage: + + >>> client.register(['message']) + {u'msg': u'', u'max_message_id': 113, u'last_event_id': -1, u'result': u'success', u'queue_id': u'1482093786:3'} + >>> client.deregister('1482093786:3') + {u'msg': u'', u'result': u'success'} + ''' + request = dict(queue_id=queue_id) + + return self.call_endpoint( + url="events", + method="DELETE", + request=request, + ) + + def get_profile(self, request=None): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + Example usage: + + >>> client.get_profile() + {u'user_id': 5, u'full_name': u'Iago', u'short_name': u'iago', ...} + ''' + return self.call_endpoint( + url='users/me', + method='GET', + request=request, + ) + + def get_streams(self, **request): + # type: (**Any) -> Dict[str, Any] + ''' + See api/examples/get-public-streams for example usage. + ''' + return self.call_endpoint( + url='streams', + method='GET', + request=request, + ) + + def get_members(self, request=None): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + See api/examples/list-members for example usage. + ''' + return self.call_endpoint( + url='users', + method='GET', + request=request, + ) + + def list_subscriptions(self, request=None): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + See api/examples/list-subscriptions for example usage. + ''' + return self.call_endpoint( + url='users/me/subscriptions', + method='GET', + request=request, + ) + + def add_subscriptions(self, streams, **kwargs): + # type: (Iterable[Dict[str, Any]], **Any) -> Dict[str, Any] + ''' + See api/examples/subscribe for example usage. + ''' + request = dict( + subscriptions=streams, + **kwargs + ) + + return self.call_endpoint( + url='users/me/subscriptions', + request=request, + ) + + def remove_subscriptions(self, streams): + # type: (Iterable[str]) -> Dict[str, Any] + ''' + See api/examples/unsubscribe for example usage. + ''' + request = dict(delete=streams) + return self.call_endpoint( + url='users/me/subscriptions', + method='PATCH', + request=request, + ) + + def get_subscribers(self, **request): + # type: (**Any) -> Dict[str, Any] + ''' + Example usage: client.get_subscribers(stream='devel') + ''' + stream = urllib.parse.quote(request['stream'], safe='') + url = 'streams/%s/members' % (stream,) + return self.call_endpoint( + url=url, + method='GET', + request=request, + ) + + def render_message(self, request=None): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + Example usage: + + >>> client.render_message(request=dict(content='foo **bar**')) + {u'msg': u'', u'rendered': u'

foo bar

', u'result': u'success'} + ''' + return self.call_endpoint( + url='messages/render', + method='GET', + request=request, + ) + + def create_user(self, request=None): + # type: (Dict[str, Any]) -> Dict[str, Any] + ''' + See api/examples/create-user for example usage. + ''' + return self.call_endpoint( + method='PUT', + url='users', + request=request, + ) class ZulipStream(object): """ @@ -534,21 +681,3 @@ class ZulipStream(object): def flush(self): # type: () -> None pass - -Client._register('send_message', url='messages', make_request=(lambda request: request)) -Client._register('update_message', method='PATCH', url='messages', make_request=(lambda request: request)) -Client._register('get_events', url='events', method='GET', longpolling=True, make_request=(lambda **kwargs: kwargs)) -Client._register('register', make_request=_mk_events) -Client._register('export', method='GET', url='export') -Client._register('deregister', url="events", method="DELETE", make_request=_mk_deregister) -Client._register('get_profile', method='GET', url='users/me') -Client._register('get_streams', method='GET', url='streams', make_request=_kwargs_to_dict) -Client._register('get_members', method='GET', url='users') -Client._register('list_subscriptions', method='GET', url='users/me/subscriptions') -Client._register('add_subscriptions', url='users/me/subscriptions', make_request=_mk_subs) -Client._register('remove_subscriptions', method='PATCH', url='users/me/subscriptions', make_request=_mk_rm_subs) -Client._register('get_subscribers', method='GET', - computed_url=lambda request: 'streams/%s/members' % (urllib.parse.quote(request['stream'], safe=''),), - make_request=_kwargs_to_dict) -Client._register('render_message', method='GET', url='messages/render') -Client._register('create_user', method='POST', url='users') diff --git a/tools/zulip-export/zulip-export b/tools/zulip-export/zulip-export index 5e19b88d82..9ce8609d0a 100755 --- a/tools/zulip-export/zulip-export +++ b/tools/zulip-export/zulip-export @@ -53,16 +53,24 @@ if options.stream == "": client.add_subscriptions([{"name": options.stream}]) queue = client.register(event_types=['message']) -client._register('get_old_messages', method='GET', url='messages') max_id = queue['max_message_id'] messages = [] +request = { + 'anchor': 0, + 'num_before': 0, + 'num_after': max_id, + 'narrow': [{'operator': 'stream', + 'operand': options.stream}], + 'apply_markdown': False, +} + print("Fetching messages...") -result = client.get_old_messages({'anchor': 0, - 'num_before': 0, - 'num_after': max_id, - 'narrow': [{'operator': 'stream', - 'operand': options.stream}], - 'apply_markdown': False}) +result = client.call_endpoint( + url='messages', + method='GET', + request=request, +) + if result['result'] != 'success': print("Unfortunately, there was an error fetching some old messages.") print(result)