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 | |
---|