diff --git a/README.md b/README.md index 61f8420..e4f4058 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ -readycloud-sample-api-client -============================ +## Python Client + +Install requirements: + pip install -r requirements.txt + +Setup a test client: + Go to the admin page and create a client. + Make sure the redirect url is set to: + http://{YOUR_SERVER}/api/v1/oauth2/auth_code + +Look at help text: + + > python readycloud.py -h + + usage: main [-h] [-l LOGFILE] [-q] [-s] [-v] [-c CLIENT_ID] [-a API_ENDPOINT] + [-r REDIRECT_URI] [-z SCOPE] + {list_orders,authorize} [{plain,csv}] + + positional arguments: + {list_orders,authorize} + The command to execute. + {plain,csv} The display format to display the data returned. + + optional arguments: + -h, --help show this help message and exit + -l LOGFILE, --logfile LOGFILE + log to file (default: log to stdout) + -q, --quiet decrease the verbosity + -s, --silent only log warnings + -v, --verbose raise the verbosity + -c CLIENT_ID, --client-id CLIENT_ID + The client id of the app. + -a API_ENDPOINT, --api-endpoint API_ENDPOINT + The default api endpoint. Defaults to + https://www.readycloud.com/api/v1/ + -r REDIRECT_URI, --redirect-uri REDIRECT_URI + Redirect uri that the client is setup for. Default is + "{API_ENDPOINT}/oauth2/auth_code" + -z SCOPE, --scope SCOPE + The requested scope of the app. Can be single or space + separated. ex "xml_backup" or "xml_backup orders" + +Example Usage: + On first run: + python readycloud.py authorize --client-id=0e6b45f3b6c962ae39f49c514f2f4d --api-endpoint https://www.readycloud.com/api/v1/ --redirect-uri https://www.readycloud.com/api/v1/oauth2/auth_code + + Follow the instructions and then after you are authorized: + python readycloud.py list_orders csv -a https://www.readycloud.com/api/v1/ + diff --git a/readycloud.py b/readycloud.py new file mode 100644 index 0000000..58f223f --- /dev/null +++ b/readycloud.py @@ -0,0 +1,228 @@ +import os, sys, json, unicodecsv +import requests +from urllib import urlencode +from cli.log import LoggingApp +from collections import OrderedDict + +API_ENDPOINT = u'https://www.readycloud.com/api/v1/' + +class Struct(object): + + def __init__(self, dict_order=None, **entries): + self.dict_order = dict_order if dict_order is not None else entries.keys() + if set(self.dict_order) > set(entries.keys()): + raise Exception(u'Headers do not match dictionary values.') + self.__dict__.update(entries) + + def keys(self): + return self.dict_order + + def values(self): + vals = [] + for o in self.dict_order: + vals.append(self.__dict__[o]) + return vals + + def items(self): + items = [] + for o in self.dict_order: + items.append((o, self.__dict__[o])) + return items + + def to_string(self): + return u'\n '.join(u'{}: {}'.format(k, str(v)) for (k, v) in self.items()) + + def name(self): + if u'id' in self.__dict__: + return u'{}: {}'.format(self.__class__.__name__, self.__dict__[u'id']) + else: + return u'{}:'.format(self.__class__.__name__) + + +class RCClient(object): + refresh_token_storage = u'refresh_token.txt' + + def __init__(self, access_token, api_endpoint, log): + self.log = log + self.api_endpoint = api_endpoint + self.resource_url_template = '{}{}/' + self.access_token = access_token + self.client = requests.session() + + def get_api_result(self, url): + """Gets a json response from the api and + converts it into a dict for use in the client. + """ + data = dict(bearer_token=self.access_token, + format=u'json') + response = self.client.get(u'{0}?{1}'.format(url, urlencode(data))) + try: + return json.loads(response.content) + except ValueError: + self.log.error(u"No data obtained. It may be that this application's access " + u"to the api has been revoked.") + return {} + + def get_resource(self, name, label, field='objects', dict_order=None): + """Gets a list of resources from the api server.""" + url = self.resource_url_template.format(self.api_endpoint, name) + result = self.get_api_result(url) + if not self.check_result(result): + return [] + new_res = [] + if field in result: + for obj in result[field]: + new_res.append(type(label, (Struct,), {})(dict_order=dict_order, **obj)) + return new_res + + def check_result(self, result): + if 'error_message' in result: + self.log.error(u'Error: {} {}'.format(result['error_message'], + u'Your access token may be revoked or invalid.')) + return False + return True + + + def get_orders(self, dict_order=None): + """Gets all orders from the api server.""" + return self.get_resource('order', 'Order', dict_order=dict_order) + + +class RCCLI(LoggingApp): + # Replace localhost:8000 here with the readycloud server + token_file = u'access_token.txt' + commands = ['list_orders', 'authorize'] + + def main(self): + + self.api_endpoint = self.params.api_endpoint + + if not self.api_endpoint.endswith('/'): + self.api_endpoint += '/' + + self.redirect_uri = self.params.redirect_uri or\ + u'{}oauth2/auth_code'.format(self.api_endpoint) + + self.auth_url = u'{}oauth2/authorize'.format(self.api_endpoint) + + if self.params.command == 'authorize': + self.authorize() + return + else: + self.read_access_token() + + self.client = RCClient(self.access_token, self.api_endpoint, self.log) + + if hasattr(self, self.params.command): + command_call = getattr(self, self.params.command) + command_call() + + def list_orders(self): + order_headers = ['id', 'primary_id', 'numerical_id', 'alias_id', + 'po_number', 'customer_number', 'source', + 'tax', 'tax_source', 'imported_tax', + 'calculated_tax', 'shipping', 'shipping_source', + 'imported_shipping', 'calculated_shipping', 'actual_shipcost', + 'status_shipped', 'ship_type', 'ship_via', 'ship_time', 'future_ship_time', + 'total', 'total_source', 'imported_total', 'calculated_total', + 'base_price', 'base_price_source', 'imported_base_price', + 'calculated_base_price', 'message', 'terms', 'resource_uri', + 'created_at', 'updated_at', 'print_time', 'order_time', + 'import_time'] + + self.print_data(self.client.get_orders(order_headers), + self.params.display_format) + + def print_data(self, data, format): + self.display_formats[format](data) + + def print_plain(self, data): + for item in data: + print(u'{}\n {}\n'.format(item.name(), item.to_string())) + + def print_csv(self, data): + w = unicodecsv.writer(sys.stdout, encoding='utf-8') + self.write_headers(data, w) + for item in data: + w.writerow(item.values()) + + def write_headers(self, data, writer): + if data: + writer.writerow(data[0].keys()) + + def setup(self): + LoggingApp.setup(self) + self.display_formats = {'csv': self.print_csv, + 'plain': self.print_plain} + + self.add_param('-c', '--client-id', default=None, dest='client_id', + help='The client id of the app.') + self.add_param('-a', '--api-endpoint', default=API_ENDPOINT, dest='api_endpoint', + help='The default api endpoint. Defaults to https://www.readycloud.com/api/v1/') + self.add_param('-r', '--redirect-uri', default=None, dest='redirect_uri', + help='Redirect uri that the client is setup for. Default is "{API_ENDPOINT}/oauth2/auth_code"') + self.add_param('-z', '--scope', default='order', dest='scope', + help=('The requested scope of the app. Can be single or space separated. ' + 'ex "xml_backup" or "xml_backup orders"')) + self.add_param('command', choices=self.commands, + help='The command to execute.') + self.add_param('display_format', default='plain', nargs='?', + choices=self.display_formats.keys(), + help='The display format to display the data returned.') + + def needs_authorization(self): + return not os.path.exists(self.token_file) + + def authorize(self): + + if not self.params.client_id: + self.log.error('You must enter a client id in order to authorize.\n') + self.argparser.print_help() + sys.exit() + + if not self.needs_authorization(): + print('You seem to have an access code already.') + ans = raw_input('Would you like to get another one? (Y/n) ') + if ans != 'Y' and ans != 'y': + print('Exiting.') + sys.exit() + + data = {'redirect_uri': self.redirect_uri, + 'client_id': self.params.client_id, + 'response_type': 'token'} + + if self.params.scope: + data['scope'] = self.params.scope + + print(u'\nTo authenticate with ReadyCloud and grant {} access to your account,\n' + u'follow this link in a web browser:'.format(__file__)) + + url = u'{}?{}'.format(self.auth_url, urlencode(data)) + + print(u'\n\t' + url) + + print('\nAfter authorizing, please paste the code displayed on the page here.') + + self.access_token = raw_input('Enter code: ') + + self.save_access_token() + + def save_access_token(self): + if self.access_token: + with open(self.token_file, 'w+') as f: + f.write(u'{}\n{}'.format(self.access_token, self.api_endpoint)) + + def read_access_token(self): + if os.path.exists(self.token_file): + with open(self.token_file) as f: + self.access_token = f.readline().strip() + self.api_endpoint = f.readline().strip() + else: + self.log.error(u'You must first call "{} authorize --client-id=YOUR_CLIENT_ID" ' + u'in order to get an access code.'.format(__file__)) + sys.exit() + + +if __name__ == '__main__': + ls = RCCLI() + ls.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3b3f376 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyCLI==2.0.3 +requests==1.1.0