0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003import os
0004import re
0005import sys
0006import urlparse
0007import urllib
0008from command import Command, BadCommand
0009from paste.deploy import loadapp, loadserver
0010from paste.wsgilib import raw_interactive
0011
0012class RequestCommand(Command):
0013
0014    min_args = 2
0015    usage = 'CONFIG_FILE URL [OPTIONS/ARGUMENTS]'
0016    takes_config_file = 1
0017    summary = "Run a request for the described application"
0018    description = """\
0019    This command makes an artifical request to a web application that
0020    uses a paste.deploy configuration file for the server and
0021    application.
0022
0023    Use 'paster request config.ini /url' to request /url.  Use
0024    'paster post config.ini /url < data' to do a POST with the given
0025    request body.
0026
0027    If the URL is relative (doesn't begin with /) it is interpreted as
0028    relative to /.command/.  The variable environ['paste.command_request']
0029    will be set to True in the request, so your application can distinguish
0030    these calls from normal requests.
0031
0032    Note that you can pass options besides the options listed here; any unknown
0033    options will be passed to the application in environ['QUERY_STRING'].
0034    """
0035
0036    parser = Command.standard_parser(quiet=True)
0037    parser.add_option('-n', '--app-name',
0038                      dest='app_name',
0039                      metavar='NAME',
0040                      help="Load the named application (default main)")
0041    parser.add_option('--config-var',
0042                      dest='config_vars',
0043                      metavar='NAME:VALUE',
0044                      action='append',
0045                      help="Variable to make available in the config for %()s substitution "
0046                      "(you can use this option multiple times)")
0047    parser.add_option('--header',
0048                      dest='headers',
0049                      metavar='NAME:VALUE',
0050                      action='append',
0051                      help="Header to add to request (you can use this option multiple times)")
0052    parser.add_option('--display-headers',
0053                      dest='display_headers',
0054                      action='store_true',
0055                      help='Display headers before the response body')
0056
0057    ARG_OPTIONS = ['-n', '--app-name', '--config-var', '--header']
0058    OTHER_OPTIONS = ['--display-headers']
0059
0060    ## FIXME: some kind of verbosity?
0061    ## FIXME: allow other methods than POST and GET?
0062
0063    _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
0064
0065    def command(self):
0066        vars = {}
0067        app_spec = self.args[0]
0068        url = self.args[1]
0069        url = urlparse.urljoin('/.command/', url)
0070        if self.options.config_vars:
0071            for item in self.option.config_vars:
0072                if ':' not in item:
0073                    raise BadCommand(
0074                        "Bad option, should be name:value : --config-var=%s" % item)
0075                name, value = item.split(':', 1)
0076                vars[name] = value
0077        headers = {}
0078        if self.options.headers:
0079            for item in self.options.headers:
0080                if ':' not in item:
0081                    raise BadCommand(
0082                        "Bad option, should be name:value : --header=%s" % item)
0083                name, value = item.split(':', 1)
0084                headers[name] = value.strip()
0085        if not self._scheme_re.search(app_spec):
0086            app_spec = 'config:'+app_spec
0087        if self.options.app_name:
0088            if '#' in app_spec:
0089                app_spec = app_spec.split('#', 1)[0]
0090            app_spec = app_spec + '#' + options.app_name
0091        app = loadapp(app_spec, relative_to=os.getcwd(), global_conf=vars)
0092        if self.command_name.lower() == 'post':
0093            request_method = 'POST'
0094        else:
0095            request_method = 'GET'
0096        qs = []
0097        for item in self.args[2:]:
0098            if '=' in item:
0099                item = urllib.quote(item.split('=', 1)[0]) + '=' + urllib.quote(item.split('=', 1)[1])
0100            else:
0101                item = urllib.quote(item)
0102            qs.append(item)
0103        qs = '&'.join(qs)
0104
0105        environ = {
0106            'REQUEST_METHOD': request_method,
0107            ## FIXME: shouldn't be static (an option?):
0108            'CONTENT_TYPE': 'text/plain',
0109            'wsgi.run_once': True,
0110            'wsgi.multithread': False,
0111            'wsgi.multiprocess': False,
0112            'wsgi.errors': sys.stderr,
0113            'QUERY_STRING': qs,
0114            'HTTP_ACCEPT': 'text/plain;q=1.0, */*;q=0.1',
0115            'paste.command_request': True,
0116            }
0117        if request_method == 'POST':
0118            environ['wsgi.input'] = sys.stdin
0119            environ['CONTENT_LENGTH'] = '-1'
0120        for name, value in headers.items():
0121            if name.lower() == 'content-type':
0122                name = 'CONTENT_TYPE'
0123            else:
0124                name = 'HTTP_'+name.upper().replace('-', '_')
0125            environ[name] = value
0126
0127        status, headers, output, errors = raw_interactive(app, url, **environ)
0128        assert not errors, "errors should be printed directly to sys.stderr"
0129        if self.options.display_headers:
0130            for name, value in headers:
0131                sys.stdout.write('%s: %s\n' % (name, value))
0132            sys.stdout.write('\n')
0133        sys.stdout.write(output)
0134        sys.stdout.flush()
0135        status_int = int(status.split()[0])
0136        if status_int != 200:
0137            return status_int
0138
0139    def parse_args(self, args):
0140        if args == ['-h']:
0141            Command.parse_args(self, args)
0142            return
0143        # These are the arguments parsed normally:
0144        normal_args = []
0145        # And these are arguments passed to the URL:
0146        extra_args = []
0147        # This keeps track of whether we have the two required positional arguments:
0148        pos_args = 0
0149        while args:
0150            start = args[0]
0151            if not start.startswith('-'):
0152                if pos_args < 2:
0153                    pos_args += 1
0154                    normal_args.append(start)
0155                    args.pop(0)
0156                    continue
0157                else:
0158                    normal_args.append(start)
0159                    args.pop(0)
0160                    continue
0161            else:
0162                found = False
0163                for option in self.ARG_OPTIONS:
0164                    if start == option:
0165                        normal_args.append(start)
0166                        args.pop(0)
0167                        if not args:
0168                            raise BadCommand(
0169                                "Option %s takes an argument" % option)
0170                        normal_args.append(args.pop(0))
0171                        found = True
0172                        break
0173                    elif start.startswith(option+'='):
0174                        normal_args.append(start)
0175                        args.pop(0)
0176                        found = True
0177                        break
0178                if found:
0179                    continue
0180                if start in self.OTHER_OPTIONS:
0181                    normal_args.append(start)
0182                    args.pop(0)
0183                    continue
0184                extra_args.append(start)
0185                args.pop(0)
0186        Command.parse_args(self, normal_args)
0187        # Add the extra arguments back in:
0188        self.args = self.args + extra_args