Added files.
This commit is contained in:
parent
a6a64fd1e6
commit
7a17fce45e
3 changed files with 279 additions and 2 deletions
51
README.md
51
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/
|
||||
|
||||
|
|
|
|||
228
readycloud.py
Normal file
228
readycloud.py
Normal file
|
|
@ -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()
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pyCLI==2.0.3
|
||||
requests==1.1.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue