From cb6ae09bc82ed6bb9e6541b228a08184df779bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20Gonz=C3=A1lez?= Date: Thu, 29 Dec 2016 02:47:22 +0000 Subject: [PATCH] api: Support file uploads to the API. Now, the `Client.do_api_query()` method supports sending files to the API. This has allowed the implementation of a new method, `Client.upload_file(file)`. It simply uploads the file set in the parameter, and returns the API's response (that includes the URI). Despite the fact that `do_api_query()` supports multiple files as parameters, `upload_file()` doesn't, because right now the API isn't capable of managing more than a file in the same request. --- examples/upload-file | 66 ++++++++++++++++++++++++++++++++++++++++++++ zulip/__init__.py | 33 ++++++++++++++++++---- 2 files changed, 93 insertions(+), 6 deletions(-) create mode 100755 examples/upload-file diff --git a/examples/upload-file b/examples/upload-file new file mode 100755 index 0000000..25e0da8 --- /dev/null +++ b/examples/upload-file @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright © 2012-2017 Zulip, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function +import importlib +import optparse +import sys + +from six.moves import StringIO as _StringIO +sys.path.insert(0, './api') +from typing import IO +import zulip + +class StringIO(_StringIO): + name = '' # https://github.com/python/typeshed/issues/598 + +usage = """upload-file --user= --api-key= [options] + +Upload a file, and print the corresponding URI. + +Example: upload-file --user=cordelia@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --file-path=cat.png + +You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc +If no --file-path is specified, a placeholder text file will be used instead. +""" + +parser = optparse.OptionParser(usage=usage) +parser.add_option('--file-path') +parser.add_option_group(zulip.generate_option_group(parser)) +(options, args) = parser.parse_args() + +client = zulip.init_from_options(options) + +file = None # type: IO +if options.file_path: + file = open(options.file_path, 'rb') +else: + file = StringIO('This is a test file.') + file.name = 'test.txt' + +response = client.upload_file(file) + +try: + print('File URI: {}'.format(response['uri'])) +except KeyError: + print('Error! API response was: {}'.format(response)) diff --git a/zulip/__init__.py b/zulip/__init__.py index 757dcc5..45a5756 100644 --- a/zulip/__init__.py +++ b/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, Iterable, Text +from typing import Any, Callable, Dict, Iterable, IO, Mapping, Optional, Text, Tuple, Union __version__ = "0.2.5" @@ -290,9 +290,13 @@ class Client(object): vendor_version=vendor_version, ) - def do_api_query(self, orig_request, url, method="POST", longpolling=False): - # type: (Mapping[str, Any], str, str, bool) -> Dict[str, Any] + def do_api_query(self, orig_request, url, method="POST", longpolling=False, files=None): + # type: (Mapping[str, Any], str, str, bool, List[IO]) -> Dict[str, Any] + if files is None: + files = [] + request = {} + req_files = [] for (key, val) in six.iteritems(orig_request): if isinstance(val, str) or isinstance(val, Text): @@ -300,6 +304,9 @@ class Client(object): else: request[key] = simplejson.dumps(val) + for f in files: + req_files.append((f.name, f)) + query_state = { 'had_error_retry': False, 'request': request, @@ -337,8 +344,12 @@ class Client(object): kwarg = "params" else: kwarg = "data" + kwargs = {kwarg: query_state["request"]} + if files: + kwargs['files'] = req_files + # Build a client cert object for requests if self.client_cert_key is not None: client_cert = (self.client_cert, self.client_cert_key) # type: Union[str, Tuple[str, str]] @@ -402,11 +413,11 @@ class Client(object): return {'msg': "Unexpected error from the server", "result": "http-error", "status_code": res.status_code} - def call_endpoint(self, url=None, method="POST", request=None, longpolling=False): - # type: (str, str, Dict[str, Any], bool) -> Dict[str, Any] + def call_endpoint(self, url=None, method="POST", request=None, longpolling=False, files=None): + # type: (str, str, Dict[str, Any], bool, List[IO]) -> Dict[str, Any] if request is None: request = dict() - return self.do_api_query(request, API_VERSTRING + url, method=method) + return self.do_api_query(request, API_VERSTRING + url, method=method, files=files) def call_on_each_event(self, callback, event_types=None, narrow=None): # type: (Callable, Optional[List[str]], Any) -> None @@ -480,6 +491,16 @@ class Client(object): request=message_data, ) + def upload_file(self, file): + # type: (IO) -> Dict[str, Any] + ''' + See api/examples/upload-file for example usage. + ''' + return self.call_endpoint( + url='user_uploads', + files=[file] + ) + def update_message(self, message_data): # type: (Dict[str, Any]) -> Dict[str, Any] '''