| 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
|---|
| 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
|---|
| 3 | import os |
|---|
| 4 | import re |
|---|
| 5 | import sys |
|---|
| 6 | import urlparse |
|---|
| 7 | import urllib |
|---|
| 8 | from command import Command, BadCommand |
|---|
| 9 | from paste.deploy import loadapp, loadserver |
|---|
| 10 | from paste.wsgilib import raw_interactive |
|---|
| 11 | |
|---|
| 12 | class RequestCommand(Command): |
|---|
| 13 | |
|---|
| 14 | min_args = 2 |
|---|
| 15 | usage = 'CONFIG_FILE URL [OPTIONS/ARGUMENTS]' |
|---|
| 16 | takes_config_file = 1 |
|---|
| 17 | summary = "Run a request for the described application" |
|---|
| 18 | description = """\ |
|---|
| 19 | This command makes an artifical request to a web application that |
|---|
| 20 | uses a paste.deploy configuration file for the server and |
|---|
| 21 | application. |
|---|
| 22 | |
|---|
| 23 | Use 'paster request config.ini /url' to request /url. Use |
|---|
| 24 | 'paster post config.ini /url < data' to do a POST with the given |
|---|
| 25 | request body. |
|---|
| 26 | |
|---|
| 27 | If the URL is relative (doesn't begin with /) it is interpreted as |
|---|
| 28 | relative to /.command/. The variable environ['paste.command_request'] |
|---|
| 29 | will be set to True in the request, so your application can distinguish |
|---|
| 30 | these calls from normal requests. |
|---|
| 31 | |
|---|
| 32 | Note that you can pass options besides the options listed here; any unknown |
|---|
| 33 | options will be passed to the application in environ['QUERY_STRING']. |
|---|
| 34 | """ |
|---|
| 35 | |
|---|
| 36 | parser = Command.standard_parser(quiet=True) |
|---|
| 37 | parser.add_option('-n', '--app-name', |
|---|
| 38 | dest='app_name', |
|---|
| 39 | metavar='NAME', |
|---|
| 40 | help="Load the named application (default main)") |
|---|
| 41 | parser.add_option('--config-var', |
|---|
| 42 | dest='config_vars', |
|---|
| 43 | metavar='NAME:VALUE', |
|---|
| 44 | action='append', |
|---|
| 45 | help="Variable to make available in the config for %()s substitution " |
|---|
| 46 | "(you can use this option multiple times)") |
|---|
| 47 | parser.add_option('--header', |
|---|
| 48 | dest='headers', |
|---|
| 49 | metavar='NAME:VALUE', |
|---|
| 50 | action='append', |
|---|
| 51 | help="Header to add to request (you can use this option multiple times)") |
|---|
| 52 | parser.add_option('--display-headers', |
|---|
| 53 | dest='display_headers', |
|---|
| 54 | action='store_true', |
|---|
| 55 | help='Display headers before the response body') |
|---|
| 56 | |
|---|
| 57 | ARG_OPTIONS = ['-n', '--app-name', '--config-var', '--header'] |
|---|
| 58 | OTHER_OPTIONS = ['--display-headers'] |
|---|
| 59 | |
|---|
| 60 | ## FIXME: some kind of verbosity? |
|---|
| 61 | ## FIXME: allow other methods than POST and GET? |
|---|
| 62 | |
|---|
| 63 | _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) |
|---|
| 64 | |
|---|
| 65 | def command(self): |
|---|
| 66 | vars = {} |
|---|
| 67 | app_spec = self.args[0] |
|---|
| 68 | url = self.args[1] |
|---|
| 69 | url = urlparse.urljoin('/.command/', url) |
|---|
| 70 | if self.options.config_vars: |
|---|
| 71 | for item in self.option.config_vars: |
|---|
| 72 | if ':' not in item: |
|---|
| 73 | raise BadCommand( |
|---|
| 74 | "Bad option, should be name:value : --config-var=%s" % item) |
|---|
| 75 | name, value = item.split(':', 1) |
|---|
| 76 | vars[name] = value |
|---|
| 77 | headers = {} |
|---|
| 78 | if self.options.headers: |
|---|
| 79 | for item in self.options.headers: |
|---|
| 80 | if ':' not in item: |
|---|
| 81 | raise BadCommand( |
|---|
| 82 | "Bad option, should be name:value : --header=%s" % item) |
|---|
| 83 | name, value = item.split(':', 1) |
|---|
| 84 | headers[name] = value.strip() |
|---|
| 85 | if not self._scheme_re.search(app_spec): |
|---|
| 86 | app_spec = 'config:'+app_spec |
|---|
| 87 | if self.options.app_name: |
|---|
| 88 | if '#' in app_spec: |
|---|
| 89 | app_spec = app_spec.split('#', 1)[0] |
|---|
| 90 | app_spec = app_spec + '#' + options.app_name |
|---|
| 91 | app = loadapp(app_spec, relative_to=os.getcwd(), global_conf=vars) |
|---|
| 92 | if self.command_name.lower() == 'post': |
|---|
| 93 | request_method = 'POST' |
|---|
| 94 | else: |
|---|
| 95 | request_method = 'GET' |
|---|
| 96 | qs = [] |
|---|
| 97 | for item in self.args[2:]: |
|---|
| 98 | if '=' in item: |
|---|
| 99 | item = urllib.quote(item.split('=', 1)[0]) + '=' + urllib.quote(item.split('=', 1)[1]) |
|---|
| 100 | else: |
|---|
| 101 | item = urllib.quote(item) |
|---|
| 102 | qs.append(item) |
|---|
| 103 | qs = '&'.join(qs) |
|---|
| 104 | |
|---|
| 105 | environ = { |
|---|
| 106 | 'REQUEST_METHOD': request_method, |
|---|
| 107 | ## FIXME: shouldn't be static (an option?): |
|---|
| 108 | 'CONTENT_TYPE': 'text/plain', |
|---|
| 109 | 'wsgi.run_once': True, |
|---|
| 110 | 'wsgi.multithread': False, |
|---|
| 111 | 'wsgi.multiprocess': False, |
|---|
| 112 | 'wsgi.errors': sys.stderr, |
|---|
| 113 | 'QUERY_STRING': qs, |
|---|
| 114 | 'HTTP_ACCEPT': 'text/plain;q=1.0, */*;q=0.1', |
|---|
| 115 | 'paste.command_request': True, |
|---|
| 116 | } |
|---|
| 117 | if request_method == 'POST': |
|---|
| 118 | environ['wsgi.input'] = sys.stdin |
|---|
| 119 | environ['CONTENT_LENGTH'] = '-1' |
|---|
| 120 | for name, value in headers.items(): |
|---|
| 121 | if name.lower() == 'content-type': |
|---|
| 122 | name = 'CONTENT_TYPE' |
|---|
| 123 | else: |
|---|
| 124 | name = 'HTTP_'+name.upper().replace('-', '_') |
|---|
| 125 | environ[name] = value |
|---|
| 126 | |
|---|
| 127 | status, headers, output, errors = raw_interactive(app, url, **environ) |
|---|
| 128 | assert not errors, "errors should be printed directly to sys.stderr" |
|---|
| 129 | if self.options.display_headers: |
|---|
| 130 | for name, value in headers: |
|---|
| 131 | sys.stdout.write('%s: %s\n' % (name, value)) |
|---|
| 132 | sys.stdout.write('\n') |
|---|
| 133 | sys.stdout.write(output) |
|---|
| 134 | sys.stdout.flush() |
|---|
| 135 | status_int = int(status.split()[0]) |
|---|
| 136 | if status_int != 200: |
|---|
| 137 | return status_int |
|---|
| 138 | |
|---|
| 139 | def parse_args(self, args): |
|---|
| 140 | if args == ['-h']: |
|---|
| 141 | Command.parse_args(self, args) |
|---|
| 142 | return |
|---|
| 143 | # These are the arguments parsed normally: |
|---|
| 144 | normal_args = [] |
|---|
| 145 | # And these are arguments passed to the URL: |
|---|
| 146 | extra_args = [] |
|---|
| 147 | # This keeps track of whether we have the two required positional arguments: |
|---|
| 148 | pos_args = 0 |
|---|
| 149 | while args: |
|---|
| 150 | start = args[0] |
|---|
| 151 | if not start.startswith('-'): |
|---|
| 152 | if pos_args < 2: |
|---|
| 153 | pos_args += 1 |
|---|
| 154 | normal_args.append(start) |
|---|
| 155 | args.pop(0) |
|---|
| 156 | continue |
|---|
| 157 | else: |
|---|
| 158 | normal_args.append(start) |
|---|
| 159 | args.pop(0) |
|---|
| 160 | continue |
|---|
| 161 | else: |
|---|
| 162 | found = False |
|---|
| 163 | for option in self.ARG_OPTIONS: |
|---|
| 164 | if start == option: |
|---|
| 165 | normal_args.append(start) |
|---|
| 166 | args.pop(0) |
|---|
| 167 | if not args: |
|---|
| 168 | raise BadCommand( |
|---|
| 169 | "Option %s takes an argument" % option) |
|---|
| 170 | normal_args.append(args.pop(0)) |
|---|
| 171 | found = True |
|---|
| 172 | break |
|---|
| 173 | elif start.startswith(option+'='): |
|---|
| 174 | normal_args.append(start) |
|---|
| 175 | args.pop(0) |
|---|
| 176 | found = True |
|---|
| 177 | break |
|---|
| 178 | if found: |
|---|
| 179 | continue |
|---|
| 180 | if start in self.OTHER_OPTIONS: |
|---|
| 181 | normal_args.append(start) |
|---|
| 182 | args.pop(0) |
|---|
| 183 | continue |
|---|
| 184 | extra_args.append(start) |
|---|
| 185 | args.pop(0) |
|---|
| 186 | Command.parse_args(self, normal_args) |
|---|
| 187 | # Add the extra arguments back in: |
|---|
| 188 | self.args = self.args + extra_args |
|---|
| 189 | |
|---|