[3] | 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 | |
---|