#!/usr/bin/env python # -*- coding: utf-8 -*- """ .(' /%/\\' (%(%))' curl' Before you can use curlish you need to register the site with the curlish client. For that you can use the --add-site parameter which will walk you through the process. example: $ curlish https://graph.facebook.com/me Notes on the authorization_code grant type: curlish spawns an HTTP server that handles a single request on http://127.0.0.1:62231/ which acts as a valid redirect target. If you need the authorization_code grant, let it redirect there. common curl options: -v verbose mode -i prints the headers -X METHOD specifies the method -H "Header: value" emits a header with a value -d "key=value" emits a pair of form data curl extension options: METHOD shortcut for -XMETHOD if it's one of the known HTTP methods. -J key=value transmits a JSON string value. -J key:=value transmits raw JSON data for a key (bool int etc.) -J @/path/to/file transmits JSON data loaded from a file. -J key=@value transmits JSON data loaded from a file for a key. --ajax Sends an X-Requested-With header with the value set to XMLHttpRequest. --cookies Enables simple cookie handling for this request. It will store cookies in ~/.ftcurlish-cookies as individual text files for each site. Use --clear-cookies to remove them. --hide-jsonp If curlish detects a JSONP response it will by default keep the wrapper function call around. If this is set it will appear as if it was a regular JSON response. """ from __future__ import with_statement import os import re import sys import cgi import webbrowser import argparse try: import json from json.encoder import JSONEncoder except ImportError: import simplejson as json from simplejson.encoder import JSONEncoder import urllib import urlparse import subprocess import base64 from copy import deepcopy from httplib import HTTPConnection, HTTPSConnection from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from getpass import getpass from uuid import UUID # Not set when frozen globals().setdefault('__file__', 'curlish.py') def str_to_uuid(s): try: UUID(s) return s except: print "%s is not a valid UUID" % s sys.exit(1) KNOWN_HTTP_METHODS = set(['GET', 'POST', 'HEAD', 'PUT', 'OPTIONS', 'TRACE', 'DELETE', 'PATCH']) DEFAULT_SETTINGS = { 'curl_path': None, 'http_port': 62231, 'json_indent': 2, 'sort_keys': True, 'colors': { 'statusline_ok': 'green', 'statusline_error': 'red', 'header': 'teal', 'brace': 'teal', 'operator': None, 'constant': 'blue', 'number': 'purple', 'string': 'yellow', 'objstring': 'green', 'jsonpfunc': None }, 'sites': { "facebook": { "extra_headers": {}, "request_token_params": { "scope": "email" }, "authorize_url": "https://www.facebook.com/dialog/oauth", "base_url": "https://graph.facebook.com/", "client_id": "384088028278656", "client_secret": "14c75a494cda2e11e8760095ec972915", "grant_type": "authorization_code", "access_token_url": "/oauth/access_token" } }, 'token_cache': {} } ANSI_CODES = { 'black': '\x1b[30m', 'blink': '\x1b[05m', 'blue': '\x1b[34m', 'bold': '\x1b[01m', 'faint': '\x1b[02m', 'green': '\x1b[32m', 'purple': '\x1b[35m', 'red': '\x1b[31m', 'reset': '\x1b[39;49;00m', 'standout': '\x1b[03m', 'teal': '\x1b[36m', 'underline': '\x1b[04m', 'white': '\x1b[37m', 'yellow': '\x1b[33m' } _list_marker = object() _value_marker = object() _jsonp_re = re.compile(r'^(.*?)\s*\((.+?)\);?\s*$(?ms)') def decode_flat_data(pairiter): def _split_key(name): result = name.split('.') for idx, part in enumerate(result): if part.isdigit(): result[idx] = int(part) return result def _enter_container(container, key): if key not in container: return container.setdefault(key, {_list_marker: False}) return container[key] def _convert(container): if _value_marker in container: force_list = False values = container.pop(_value_marker) if container.pop(_list_marker): force_list = True values.extend(_convert(x[1]) for x in sorted(container.items())) if not force_list and len(values) == 1: values = values[0] return values elif container.pop(_list_marker): return [_convert(x[1]) for x in sorted(container.items())] return dict((k, _convert(v)) for k, v in container.iteritems()) result = {_list_marker: False} for key, value in pairiter: parts = _split_key(key) if not parts: continue container = result for part in parts: last_container = container container = _enter_container(container, part) last_container[_list_marker] = isinstance(part, (int, long)) container[_value_marker] = [value] return _convert(result) def get_color(element): user_colors = settings.values['colors'] name = user_colors.get(element) if name is None and element not in user_colors: name = DEFAULT_SETTINGS['colors'].get(element) if name is not None: return ANSI_CODES.get(name, '') return '' def isatty(stream): """Is stdout connected to a terminal or a file?""" if not hasattr(stream, 'isatty'): return False if not stream.isatty(): return False return True def is_color_terminal(stream=None): """Returns `True` if this terminal has colors.""" if stream is None: stream = sys.stdout if not isatty(stream): return False if 'COLORTERM' in os.environ: return True term = os.environ.get('TERM', 'dumb').lower() if term in ('xterm', 'linux') or 'color' in term: return True return False def fail(message): """Fails with an error message.""" print >> sys.stderr, 'error:', message sys.exit(1) def find_url_arg(arguments): """Finds the URL argument in a curl argument list.""" for idx, arg in enumerate(arguments): if arg.startswith(('http:', 'https:')): return idx class AuthorizationHandler(BaseHTTPRequestHandler): """Callback handler for the code based authorization""" def do_GET(self): self.send_response(200, 'OK') self.send_header('Content-Type', 'text/html') self.end_headers() self.server.token_response = dict((k, v[-1]) for k, v in cgi.parse_qs(self.path.split('?')[-1]).iteritems()) if 'code' in self.server.token_response: title = 'Tokens Received' text = 'The tokens were transmitted successfully to curlish.' else: title = 'Error on Token Exchange' text = 'Could not exchange tokens :-(' self.wfile.write(''' <!doctype html> <title>%(title)s</title> <style type=text/css> body { font-family: sans-serif; margin: 60px auto; width: 400px; } h1 { font-weight: normal; size: 28px; color: #b00; margin: 0 0 15px 0; } p { margin: 7px 0 0 20px; } </style> <h1>%(title)s</h1> <p>%(text)s <p>You can now close this window, it's no longer needed. ''' % locals()) self.wfile.close() def log_message(self, *args, **kwargs): pass def get_cookie_path(): if os.name == 'nt': return os.path.expandvars(r'%APPDATA%\\FireteamCurlish\\Cookies') return os.path.expanduser(r'~/.ftcurlish-cookies') class Settings(object): """Wrapper around the settings file""" def __init__(self): if os.name == 'nt': self.filename = os.path.expandvars(r'%APPDATA%\\FireteamCurlish\\config.json') else: self.filename = os.path.expanduser(r'~/.ftcurlish.json') rv = deepcopy(DEFAULT_SETTINGS) if os.path.isfile(self.filename): with open(self.filename) as f: try: rv.update(json.load(f)) except Exception, e: fail('Error: JSON error in config file: %s' % e) if not rv['curl_path']: rv['curl_path'] = get_default_curl_path() self.values = rv def save(self): dirname = os.path.dirname(self.filename) try: os.makedirs(dirname) except OSError: pass with open(self.filename, 'w') as f: json.dump(self.values, f, indent=2) class Site(object): """Represents a single site.""" def __init__(self, name, values): def _full_url(url): if self.base_url is not None: return urlparse.urljoin(self.base_url, url) return url self.name = name self.base_url = values.get('base_url') self.grant_type = values.get('grant_type', 'authorization_code') self.access_token_url = _full_url(values.get('access_token_url')) self.authorize_url = _full_url(values.get('authorize_url')) self.client_id = values.get('client_id') self.client_secret = values.get('client_secret') self.request_token_params = values.get('request_token_params') or {} self.extra_headers = values.get('extra_headers') or {} self.bearer_transmission = values.get('bearer_transmission', 'query') self.default = values.get('default', False) self.access_token = None def make_request(self, method, url, headers=None, data=None): """Makes an HTTP request to the site.""" u = urlparse.urlparse(url) pieces = u.netloc.rsplit(':', 1) secure = u.scheme == 'https' host = pieces[0].strip('[]') if len(pieces) == 2 and pieces[-1].isdigit(): port = int(pieces[-1]) else: port = secure and 443 or 80 conncls = secure and HTTPSConnection or HTTPConnection conn = conncls(host, port) if isinstance(data, dict): data = urllib.urlencode(data) real_headers = self.extra_headers.copy() real_headers.update(headers or ()) conn.request(method, u.path, data, real_headers) resp = conn.getresponse() ct = resp.getheader('Content-Type') if ct.startswith('application/json') or ct.startswith('text/javascript'): resp_data = json.loads(resp.read()) elif ct.startswith('text/html'): fail('Invalid response from server: ' + resp.read()) else: resp_data = dict((k, v[-1]) for k, v in cgi.parse_qs(resp.read()).iteritems()) return resp.status, resp_data def get_access_token(self, params): """Tries to load tokens with the given parameters.""" data = params.copy() # Provide the credentials both as a basic authorization header as well as # the parameters in the URL. Should make everybody happy. At least I hope so. data['client_id'] = self.client_id data['client_secret'] = self.client_secret creds = self.client_id + ':' + self.client_secret headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Basic ' + base64.b64encode(creds)} status, data = self.make_request('POST', self.access_token_url, data=data, headers=headers) if status in (200, 201): return data['access_token'] error = data.get('error') or 'unknown_error' if error in ('invalid_grant', 'access_denied'): return None error_msg = data.get('error_description') or 'no description' fail("Couldn't authorize: %s: %s" % (error, error_msg)) def request_password_grant(self): while 1: params = {'grant_type': 'password'} params['username'] = raw_input('Username: ') params['password'] = getpass() params.update(self.request_token_params) rv = self.get_access_token(params) if rv is None: print 'Error: invalid credentials' continue settings.values['token_cache'][self.name] = rv return def request_client_credentials_grant(self): params = {'grant_type': 'client_credentials'} params.update(self.request_token_params) rv = self.get_access_token(params) if rv is None: print 'Error: client_credentials token request failed' else: settings.values['token_cache'][self.name] = rv def request_authorization_code_grant(self): redirect_uri = 'http://127.0.0.1:%d/' % settings.values['http_port'] params = { 'client_id': self.client_id, 'redirect_uri': redirect_uri, 'response_type': 'code' } params.update(self.request_token_params) browser_url = '%s?%s' % ( self.authorize_url, urllib.urlencode(params) ) webbrowser.open(browser_url) server_address = ('127.0.0.1', settings.values['http_port']) httpd = HTTPServer(server_address, AuthorizationHandler) httpd.token_response = None httpd.handle_request() if 'code' in httpd.token_response: return self.exchange_code_for_token(httpd.token_response['code'], redirect_uri) print 'Could not sign in: grant cancelled' for key, value in httpd.token_response.iteritems(): print ' %s: %s' % (key, value) sys.exit(1) def exchange_code_for_token(self, code, redirect_uri): settings.values['token_cache'][self.name] = self.get_access_token({ 'code': code, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri }) def request_tokens(self): if self.grant_type == 'password': self.request_password_grant() elif self.grant_type == 'authorization_code': self.request_authorization_code_grant() elif self.grant_type == 'client_credentials': self.request_client_credentials_grant() else: fail('Invalid grant configured: %s' % self.grant_type) def fetch_token_if_necessarys(self): token_cache = settings.values['token_cache'] if token_cache.get(self.name) is None: self.request_tokens() self.access_token = token_cache[self.name] def get_site_by_name(name): """Finds a site by its name.""" rv = settings.values['sites'].get(name) if rv is not None: return Site(name, rv) def get_site(site_name, url_arg): """Tries to look up a site from the config or automatically.""" if site_name is not None: site = get_site_by_name(site_name) if site is not None: return site fail('Site %s does not exist' % site_name) matches = [] for name, site in settings.values['sites'].iteritems(): base_url = site.get('base_url') if base_url and url_arg.startswith(base_url): matches.append(Site(name, site)) if len(matches) == 1: return matches[0] for match in matches: if match.default: return match if len(matches) > 1: fail('Too many matches. Please specificy an application ' 'explicitly or set a default') def get_default_curl_path(): """Tries to find curl and returns the path to it.""" def tryrun(path): try: subprocess.call([path, '--version'], stdout=subprocess.PIPE, stdin=subprocess.PIPE) except OSError: return False return True if tryrun('curl'): return 'curl' base = os.path.abspath(os.path.dirname(__file__)) for name in 'curl', 'curl.exe': fullpath = os.path.join(base, name) print fullpath if tryrun(fullpath): return fullpath def colorize_json_stream(iterator): """Adds colors to a JSON event stream.""" for event in iterator: color = None e = event.strip() if e in '[]{}': color = get_color('brace') elif e in ',:': color = get_color('operator') elif e[:1] == '"': color = get_color('string') elif e in ('true', 'false', 'null'): color = get_color('constant') else: color = get_color('number') if color is not None: event = color + event + ANSI_CODES['reset'] yield event def print_formatted_json(json_data, jsonp_func=None, stream=None): """Reindents JSON and colorizes if wanted. We use our own wrapper around json.dumps because we want to inject colors and the simplejson iterator encoder does some buffering between separate events that makes it really hard to inject colors. """ if stream is None: stream = sys.stdout if is_color_terminal(stream): def colorize(colorname, text): color = get_color(colorname) reset = ANSI_CODES['reset'] return color + text + reset else: colorize = lambda x, t: t def _walk(obj, indentation, inline=False, w=stream.write): i = ' ' * (indentation * settings.values['json_indent']) if not inline: w(i) if isinstance(obj, basestring): w(colorize('string', json.dumps(obj))) elif isinstance(obj, (int, long, float)): w(colorize('number', json.dumps(obj))) elif obj in (True, False, None): w(colorize('constant', json.dumps(obj))) elif isinstance(obj, list): if not obj: w(colorize('brace', '[]')) else: w(colorize('brace', '[\n')) for idx, item in enumerate(obj): if idx: w(colorize('operator', ',\n')) _walk(item, indentation + 1) w(colorize('brace', '\n' + i + ']')) elif isinstance(obj, dict): if not obj: w(colorize('brace', '{}')) else: w(colorize('brace', '{\n')) if settings.values['sort_keys']: items = sorted(obj.items(), key=lambda x: x[0].lower()) else: items = obj.iteritems() for idx, (key, value) in enumerate(items): if idx: w(colorize('operator', ',\n')) ki = i + ' ' * settings.values['json_indent'] w(ki + colorize('objstring', json.dumps(key))) w(colorize('operator', ': ')) _walk(value, indentation + 1, inline=True) w(i + colorize('brace', '\n' + i + '}')) else: # hmm. should not happen, but let's just assume it might # because of json changes w(json.dumps(obj)) if jsonp_func is not None: stream.write(colorize('jsonpfunc', jsonp_func)) stream.write(colorize('brace', '(')) _walk(json_data, 0) stream.write(colorize('brace', ')')) stream.write(colorize('operator', ';')) else: _walk(json_data, 0) stream.write('\n') stream.flush() def beautify_curl_output(p, hide_headers, hide_jsonp=False, stream=None, json_stream=False): """Parses curl output and adds colors and reindents as necessary.""" if stream is None: stream = sys.stdout json_body = False might_be_javascript = False has_colors = is_color_terminal() # Headers while 1: line = p.stdout.readline() if not line: break if has_colors and re.search(r'^HTTP/', line): if re.search('HTTP/\d+.\d+ [45]\d+', line): color = get_color('statusline_error') else: color = get_color('statusline_ok') if not hide_headers: stream.write(color + line + ANSI_CODES['reset']) continue if re.search(r'^Content-Type:\s*(text/javascript|application/(.+?\+)?json)\s*(?i)', line): json_body = True if 'javascript' in line: might_be_javascript = True if not hide_headers: # Nicer headers if we detect them if not line.startswith(' ') and ':' in line: key, value = line.split(':', 1) else: key = None if has_colors and key is not None: stream.write(get_color('header') + key + ANSI_CODES['reset'] + ': ' + value.lstrip()) else: stream.write(line) stream.flush() if line == '\r\n': break # JSON streams if json_stream: while 1: line = p.stdout.readline() if not line: break line = line.strip() if line: try: data = json.loads(line) except Exception: print 'invalid json:', line else: print_formatted_json(data, stream=stream) return iterable = p.stdout # JSON Body. Do not reindent if we have headers and are piping # into a file because of changing content length. if json_body and (hide_headers or isatty(stream)): body = ''.join(iterable) json_body = body jsonp_func = None if might_be_javascript: jsonp_match = _jsonp_re.match(body) if jsonp_match is not None: if not hide_jsonp: jsonp_func = jsonp_match.group(1) json_body = jsonp_match.group(2) try: data = json.loads(json_body) except Exception: # Something went wrong, it's malformed. Just make it an # iterable again and print it normally; iterable = body.splitlines(True) else: print_formatted_json(data, jsonp_func, stream) return # Regular body for line in iterable: stream.write(line) stream.flush() def clear_token_cache(site_name): """Delets all tokens or the token of a site.""" site = None if site_name is not None: site = get_site_by_name(site_name) if site is None: fail('Site %s does not exist' % site_name) if site is None: settings.values['token_cache'] = {} print 'Cleared the token cache' else: settings.values['token_cache'].pop(site.name, None) print 'Cleared the token cache for %s' % site.name settings.save() def init_config(): """Initializes the config""" print 'Initialized the config in %s' % settings.filename settings.save() def add_site(site_name): """Registers a new site with the config.""" def prompt(prompt, one_of=None, default=None): if default is not None: prompt += ' [%s]' % default if one_of: prompt += ' (options=%s)' % ', '.join(sorted(one_of)) while 1: value = raw_input(prompt + ': ') if value: if one_of and value not in one_of: print 'error: invalid value' continue return value if default is not None: return default authorize_url = None base_url = prompt('base_url') if prompt('Configure OAuth 2.0?', ['yes', 'no'], 'yes') == 'yes': grant_type = prompt('grant_type', one_of=['password', 'authorization_code'], default='authorization_code') access_token_url = prompt('access_token_url') if grant_type == 'authorization_code': authorize_url = prompt('authorize_url') client_id = prompt('client_id') client_secret = prompt('client_secret') bearer_transmission = prompt('bearer_transmission', one_of=['header', 'query'], default='query') else: grant_type = None access_token_url = None client_id = None client_secret = None bearer_transmission = None settings.values['sites'][site_name] = { 'extra_headers': {}, 'request_token_params': {}, 'base_url': base_url, 'grant_type': grant_type, 'base_url': base_url, 'access_token_url': access_token_url, 'authorize_url': authorize_url, 'client_id': client_id, 'client_secret': client_secret, 'grant_type': grant_type, 'bearer_transmission': bearer_transmission, 'default': False } settings.values['token_cache'].pop(site_name, None) settings.save() print 'Site %s added' % site_name def remove_site(site_name): """Removes a site from the config.""" try: settings.values['sites'].pop(site_name) except KeyError: fail('Site %s does not exist' % site_name) settings.save() print 'Site %s removed' % site_name def list_sites(): """Prints a list of all sites.""" print 'Registered sites:' print for name, site in sorted(settings.values['sites'].items()): print ' %s' % name for key, value in sorted(site.items()): if isinstance(value, dict): print ' %s:%s' % (key, not value and ' -' or '') for key, value in sorted(value.items()): print ' %s: %s' % (key, value) else: print ' %s: %s' % (key, value) print def clear_cookies(site_name): if site_name is None: import shutil try: shutil.rmtree(get_cookie_path()) except (OSError, IOError): pass print 'Deleted all cookies' return if site_name not in settings.values['sites']: fail('Site %s does not exist' % site_name) try: os.remove(os.path.join(get_cookie_path(), site_name + '.txt')) except OSError: pass print 'Cookies for %s deleted' % site_name def add_content_type_if_missing(args, content_type): """Very basic hack that adds a content type if no content type was mentioned so far. """ was_h = False for arg in args: iarg = arg.lower() if iarg.startswith('-hcontent-type'): return elif iarg == '-h': was_h = True elif was_h: if iarg.startswith('content-type'): return was_h = False args.append('-H') args.append('Content-Type: ' + content_type) def handle_curlish_arguments(site, args): new_args = [] json_pairs = [] use_cookies = False hide_jsonp = False argiter = iter(args) def _get_next_arg(error): try: return argiter.next() except StopIteration: fail('Error: ' + error) def handle_json_value(value): dkey = '' def _load_json_value(filename): try: with open(filename) as f: return json.load(f) except IOError as e: fail('Error: could not read from file: %s' % e) except Exception as e: fail('Error: invalid JSON data for "%s"' % dkey) if value[:1] == '@': value = _load_json_value(value[1:]) else: args = value.split('=', 1) if len(args) < 2: fail('Error: wrong argument count for -J') dkey, value = args if dkey.endswith(':'): dkey = dkey[:-1] try: value = json.loads(value) except Exception: fail('Error: invalid JSON data for "%s"' % dkey) elif value[:1] == '@': value = _load_json_value(value[1:]) json_pairs.append((dkey, value)) last_arg_was_x = False for idx, arg in enumerate(argiter): # Automatic -X in front of known http method names if arg in KNOWN_HTTP_METHODS and not last_arg_was_x: new_args.append('-X' + arg) # Shortcut for X-Requested-With elif arg == '--ajax': new_args.append('-H') new_args.append('X-Requested-With: XMLHttpRequest') # Cookie support elif arg == '--cookies': use_cookies = True # Hide JSONP function name? elif arg == '--hide-jsonp': hide_jsonp = True # JSON data elif arg == '-J': handle_json_value(_get_next_arg('-J requires an argument')) elif arg.startswith('-J'): handle_json_value(arg[2:]) # Regular argument else: new_args.append(arg) last_arg_was_x = arg == '-X' json_data = decode_flat_data(json_pairs) need_json = bool(json_data) if len(json_data) == 1 and '' in json_data: json_data = json_data[''] if need_json: add_content_type_if_missing(new_args, 'application/json') new_args.append('--data-binary') new_args.append(json.dumps(json_data)) if use_cookies: cookie_path = get_cookie_path() if not os.path.isdir(cookie_path): os.makedirs(cookie_path) if site is None: cookie_filename = os.path.join(cookie_path, '_default.txt') else: cookie_filename = os.path.join(cookie_path, site.name + '.txt') new_args.extend(( '-c', cookie_filename, '-b', cookie_filename )) return new_args, {'hide_jsonp': hide_jsonp} def invoke_curl(site, curl_path, args, url_arg, dump_args=False, dump_response=None, json_stream=False): if args[0] == '--': args.pop(0) if not curl_path: fail('Could not find curl. Put it into your config') url = args[url_arg] if site is not None and site.bearer_transmission is not None: if site.bearer_transmission == 'header': args += ['-H', 'Authorization: Bearer %s' % site.access_token] elif site.bearer_transmission == 'query': url += ('?' in url and '&' or '?') + 'access_token=' + \ urllib.quote(site.access_token) else: fail('Bearer transmission %s is unknown.' % site.bearer_transmission) args[url_arg] = url if site is not None: for key, value in site.extra_headers.iteritems(): args += ['-H', '%s: %s' % (key, value)] # Force response headers hide_headers = False if '-i' not in args: args.append('-i') hide_headers = True # Hide stats but keep errors args.append('-sS') # Unbuffered args.append('-N') # Disable expect by default args.append('-HExpect:') # Handle curlish specific argument shortcuts args, options = handle_curlish_arguments(site, args) if dump_args: print ' '.join('"%s"' % x.replace('"', '\\"') if any(y.isspace() for y in x) else x for x in args) return p = subprocess.Popen([curl_path] + args, stdout=subprocess.PIPE, bufsize=1) if dump_response is not None: f = open(dump_response, 'w') else: f = sys.stdout beautify_curl_output(p, hide_headers, hide_jsonp=options['hide_jsonp'], stream=f, json_stream=json_stream) if f is not sys.stdout: f.close() sys.exit(p.wait()) # Load the settings once before we start up settings = Settings() def main(): parser = argparse.ArgumentParser(description="curl, with flames on top", add_help=False) parser.add_argument('-h', '--help', action='store_true', help='Prints this help.') parser.add_argument('--init-config', action='store_true', help='Adds an empty config if it is currently ' 'missing.') parser.add_argument('--site', help='The site to use. By default it will ' 'guess the site from the URL of the request preferring ' 'sites with default set to True.') parser.add_argument('--clear-token-cache', action='store_true', help='Clears the token cache. By default of all the ' 'sites, can be limited to one site with --site.') parser.add_argument('--add-site', help='Registers a new site with curlish.', metavar='NAME') parser.add_argument('--remove-site', help='Unregisters a site from curlish.', metavar='NAME') parser.add_argument('--list-sites', help='Lists all known sites', action='store_true') parser.add_argument('--clear-cookies', action='store_true', help='Deletes all the cookies or cookies that belong ' 'to one specific site only.') parser.add_argument('--dump-curl-args', action='store_true', help='Instead of executing dump the curl command line ' 'arguments for this call') parser.add_argument('--dump-response', help='Instead of writing the response ' 'to stdout, write the response into a file instead') parser.add_argument('--json-stream', action='store_true', default=False, help='Assumes that the response from the server is a JSON ' 'response stream and colorizes each element ' 'individually and skips past empty chunks.') try: args, extra_args = parser.parse_known_args() except Exception, e: print e sys.exit(1) if args.help: parser.print_help() print __doc__.rstrip() return # Custom commands if args.clear_token_cache: clear_token_cache(args.site) return if args.init_config: init_config() return if args.add_site: add_site(args.add_site) return if args.remove_site: remove_site(args.remove_site) return if args.list_sites: list_sites() return if args.clear_cookies: clear_cookies(args.site) return # Redirect everything else to curl via the site url_arg = find_url_arg(extra_args) if url_arg is None: parser.print_usage() return site = get_site(args.site, extra_args[url_arg]) if site is not None and site.grant_type is not None: site.fetch_token_if_necessarys() settings.save() invoke_curl(site, settings.values['curl_path'], extra_args, url_arg, dump_args=args.dump_curl_args, dump_response=args.dump_response, json_stream=args.json_stream) if __name__ == '__main__': try: main() except KeyboardInterrupt: pass