-
Notifications
You must be signed in to change notification settings - Fork 10
/
request.py
602 lines (510 loc) · 23.1 KB
/
request.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
import sublime
import sublime_plugin
from requests import options
import os
import json
import time
from sys import maxsize
from urllib import parse
from collections import namedtuple
from .graphql import set_graphql_schema_on_view
from ..core import RequestCommandMixin
from ..core.parsers import parse_requests
from ..core.responses import prepare_request
from ..core.helpers import clean_url, truncate
Content = namedtuple('Content', 'content, point')
platform = sublime.platform()
def response_tab_bindings(can_save=False):
"""Returns string with special key bindings for response tab commands.
"""
replay = '[cmd+r]' if platform == 'osx' else '[ctrl+r]'
nav = '[ctrl+alt+ ←/→]'
pin = '[cmd+t]' if platform == 'osx' else '[ctrl+t]'
save = '[cmd+s]' if platform == 'osx' else '[ctrl+s]'
explore = '[cmd+e]' if platform == 'osx' else '[ctrl+e]'
return '{} replay request, {} prev/next request, {} pin/unpin tab, {} save request, {} explore URL'.format(
replay, nav, pin, save, explore)
def get_content(res, fmt):
"""Efficiently decides if response content is binary. If this is the case,
returns before `text` or `json` are invoked on response, because they are VERY
SLOW when invoked on binary responses. See below:
https://github.com/requests/requests/issues/2371
Also makes sure response isn't too large to display. Finally, prettifies JSON
if response content is JSON.
"""
config = sublime.load_settings('Requester.sublime-settings')
max_len = 1000 * int(config.get('max_content_length_kb', 5000))
if len(res.content) > max_len:
return "Response is too big. This might be a binary file, or you might need to increase " +\
"`max_content_length_kb` in Requester's settings."
if res.encoding is None:
try:
res.content.decode('utf-8')
except UnicodeDecodeError:
# content is almost certainly binary, and if it's not, requests won't know how to decode it anyway
return "Response content is binary, so it won't be displayed. Try downloading this file instead."
else:
res.encoding = 'utf-8'
try:
json_dict = res.json()
except:
return res.text
else:
if fmt == 'indent_sort':
return json.dumps(json_dict, sort_keys=True, indent=2, separators=(',', ': '))
if fmt == 'indent':
return json.dumps(json_dict, indent=2, separators=(',', ': '))
return res.text
def get_response_view_content(response):
"""Returns a response string that includes metadata, headers and content,
and the index of the string at which response content begins.
"""
req, res, err = response
read_content = True
if 'filename' in req.skwargs:
read_content = False
redirects = [response.url for response in res.history] # URLs traversed due to redirects
redirects.append(res.url) # final URL
header = '{} {}\n{}s, {}B\n{}'.format(
res.status_code, res.reason, res.elapsed.total_seconds(), len(res.content) if read_content else '?',
' -> '.join(redirects)
)
headers = '\n'.join(
['{}: {}'.format(k, v) for k, v in sorted(res.headers.items())]
)
content = get_content(res, req.skwargs.get('fmt')) if read_content else 'File download.'
before_content_items = [
req.request,
header,
'Request Headers: {}'.format(res.request.headers),
response_tab_bindings(),
headers
]
try:
body = parse.parse_qs(res.request.body)
body = {k: v[0] for k, v in body.items()}
except:
body = res.request.body
cookies = res.cookies.get_dict()
if cookies:
before_content_items.insert(3, 'Response Cookies: {}'.format(cookies))
if body:
before_content_items.insert(3, 'Request Body: {}'.format(truncate(body, 1000)))
before_content = '\n\n'.join(before_content_items)
return Content(before_content + '\n\n' + content, len(before_content) + 2)
def set_response_view_name(view, res=None):
"""Set name for `view` with content from `response`.
"""
config = sublime.load_settings('Requester.sublime-settings')
max_len = int(config.get('response_tab_name_length', 32))
try: # short but descriptive, to facilitate navigation between response tabs, e.g. using Goto Anything
path = parse.urlparse(res.url).path
if path and path[-1] == '/':
path = path[:-1]
name = '{}: {}'.format(res.request.method, path)
except:
name = view.settings().get('requester.name')
else:
view.settings().set('requester.name', name)
pinned = view.settings().get('requester.response_pinned', False)
view.set_name('{}{}'.format('** ' if pinned else '', truncate(name, max_len)))
class RequestsMixin:
def get_requests(self):
"""Parses requests from multiple selections. If nothing is highlighted,
cursor's current line is taken as selection.
"""
view = self.view
requests = []
for region in view.sel():
if not region.empty():
selection = view.substr(region)
try:
requests_ = parse_requests(selection)
except Exception as e:
sublime.error_message('Parse Error: there may be unbalanced parentheses in calls to requests')
print(e)
continue
else:
selection = view.substr(view.line(region))
extended_selection = view.substr(sublime.Region(view.line(region).a, view.size()))
try:
requests_ = parse_requests(selection, n=1, es=extended_selection)
except Exception as e:
sublime.error_message('Parse Error: there may be unbalanced parentheses in calls to requests')
print(e)
continue
for request in requests_:
requests.append(request)
return requests
def show_activity_for_pending_requests(self, requests, count, activity):
"""If there are already open response views waiting to display content from
pending requests, show activity indicators in views.
"""
for req in requests:
for view in self.response_views_with_matching_request(
req.method, req.url
):
# view names set BEFORE view content is set, otherwise
# activity indicator in view names seems to lag a little
name = view.settings().get('requester.name')
if not name:
view.set_name(activity)
else:
spaces = min(self.ACTIVITY_SPACES, len(name))
activity = self.get_activity_indicator(count, spaces)
extra_spaces = 4 # extra spaces because tab names don't use monospace font =/
view.set_name(activity.ljust(len(name) + extra_spaces))
view.run_command('requester_replace_view_text', {'text': '{}\n\n{}\n'.format(
req.request, activity
)})
break # do this for first view only
def response_views_with_matching_request(self, method, url):
"""Get all response views whose request matches `request`.
"""
if self.view.settings().get('requester.response_view', False):
return [self.view] # don't update other views when replaying a request
views = []
for sheet in self.view.window().sheets():
view = sheet.view()
if not view:
continue
if view.settings().get('requester.response_pinned', False):
continue
if view.settings().get('requester.response_view', False):
view_method = view.settings().get('requester.request_method', None)
view_url = view.settings().get('requester.request_url', None)
if not view_method or not view_url:
continue
if method == view_method and clean_url(url) == clean_url(view_url):
views.append(view)
return views
def handle_response(self, response):
"""Create a response view. Ensure that response tab comes after (to the
right of) all other response tabs.
Don't create new response tab if a response tab matching request is open.
"""
window = self.view.window()
req, res, err = response
if res is None or err:
return
method, url = res.request.method, res.url
requester_sheet = window.active_sheet()
last_sheet = requester_sheet # find last sheet (tab) with a response view
for sheet in window.sheets():
view = sheet.view()
if view and view.settings().get('requester.response_view', False):
last_sheet = sheet
window.focus_sheet(last_sheet) # make sure new tab is opened after last open response view
views = self.response_views_with_matching_request(method, url)
if not len(views): # if there are no matching response tabs, create a new one
view = window.new_file()
pinned = self.config.get('pin_tabs_by_default', False)
if pinned:
view.settings().set('requester.response_pinned', True)
else:
view = views[0]
window.focus_sheet(requester_sheet) # keep focus on requester view
self.prepare_response_view(view, response)
def prepare_response_view(self, view, response):
"""Insert response content into response view, and set settings on it.
"""
req, res, err = response
self._response_view = view # cache this to change focus after all responses return
view.set_scratch(True)
# this setting allows keymap to target response views separately
view.settings().set('requester.response_view', True)
self.set_env_on_view(view)
content, point = get_response_view_content(response)
view.run_command('requester_replace_view_text', {'text': content, 'point': point})
view.set_syntax_file('Packages/Requester/syntax/requester-response.sublime-syntax')
set_request_on_view(view, res)
# should response tabs be reordered after requests return?
if self.config.get('reorder_tabs_after_requests', False):
self.view.run_command('requester_reorder_response_tabs')
set_response_view_name(view, res)
set_graphql_schema_on_view(view, req)
set_save_info_on_view(view, req.request)
def handle_responses(self, responses):
"""Change focus after request returns? `handle_response` must be called
before this method.
"""
if len(responses) != 1:
return
if not self.config.get('change_focus_after_request', True):
return
if not responses[0].err and responses[0].res is not None:
if hasattr(self, '_response_view'):
self.view.window().focus_view(self._response_view)
class RequesterCommand(RequestsMixin, RequestCommandMixin, sublime_plugin.TextCommand):
"""Execute requests from requester file concurrently and open multiple
response views.
"""
def run(self, edit, concurrency=10):
"""Allow user to specify concurrency.
"""
self.MAX_WORKERS = max(1, concurrency)
super().run(edit)
class RequesterReplayRequestCommand(RequestsMixin, RequestCommandMixin, sublime_plugin.TextCommand):
"""Replay a request from a response view.
"""
def get_requests(self):
"""Only parses first request in file.
"""
try:
request = self.get_replay_request()
except:
return []
return [request]
def get_replay_request(self):
"""Only parses first request in file.
"""
try:
requests = parse_requests(self.view.substr(sublime.Region(0, self.view.size())), n=1)
except Exception as e:
sublime.error_message('Parse Error: there may be unbalanced parentheses in your request')
print(e)
raise
try:
return requests[0]
except IndexError:
sublime.error_message('Replay Error: there is no request in your response review')
raise
def handle_response(self, response):
"""Overwrites content in current view.
"""
view = self.view
req, res, err = response
if err:
return
content, point = get_response_view_content(response)
view.run_command('requester_replace_view_text', {'text': content, 'point': point})
view.set_syntax_file('Packages/Requester/syntax/requester-response.sublime-syntax')
set_request_on_view(view, res)
view.settings().erase('requester.request_history_index')
view.settings().set('requester.history_view', False)
set_response_view_name(view, res)
set_graphql_schema_on_view(view, req)
class RequesterExploreUrlCommand(RequesterReplayRequestCommand):
"""Explore a new URL from a response view. If the request doesn't hit the same
domain, remove the `headers`, `cookies` and `auth` args. This makes it trivial
for users to explore hyperlinked APIs (HATEOAS).
"""
def get_requests(self):
"""Parses URL from first selection, and passes it in special `explore` arg
to call to requests.
"""
view = self.view
if not view or not view.settings().get('requester.response_view', False):
sublime.error_message('Explore Error: you can only explore URLs from response tabs')
return []
try:
url = view.substr(view.sel()[0]).replace('"', '')
except:
return []
if not url:
return []
self._explore_url = url
try:
request = self.get_replay_request()
except:
return []
return ["{}, explore=({}, {}))".format(request[:-1], repr(request), repr(url))]
def show_activity_for_pending_requests(self, *args, **kwargs):
"""Don't do this for exploratory requests.
"""
def handle_response(self, response):
"""Creates new "explore URL" view.
"""
req, res, err = response
if err:
return
view = self.view.window().new_file()
self.set_env_on_view(view)
view.settings().set('requester.response_view', True)
view.set_scratch(True)
content, point = get_response_view_content(response)
view.run_command('requester_replace_view_text', {'text': content, 'point': point})
view.set_syntax_file('Packages/Requester/syntax/requester-response.sublime-syntax')
set_request_on_view(view, res)
set_response_view_name(view, res)
set_graphql_schema_on_view(view, req)
def persist_requests(self, responses):
"""Don't do this for exploratory requests.
"""
class RequesterUrlOptionsCommand(sublime_plugin.WindowCommand):
"""Display pop-up with options for request in currently open response tab.
"""
def run(self):
view = self.window.active_view()
url = view.settings().get('requester.request_url', None)
if url is None:
return
sublime.set_timeout_async(lambda: self.show_options(url, view), 0)
def show_options(self, url, view):
"""Send options request to `url` and display results in pop-up.
"""
res = options(url, timeout=3)
if not res.ok:
return
names = ['Allow', 'Access-Control-Allow-Methods', 'Access-Control-Max-Age']
headers = [res.headers.get(name, None) for name in names]
items = '\n'.join('<li>{}: {}</li>'.format(n, h) for n, h in zip(names, headers) if h)
content = '<h2>OPTIONS: {}</h2>\n<ul>{}</ul>'.format(url, items)
try:
json_dict = res.json()
except:
pass
else:
content = '{}\n<pre><code>{}</pre></code>'.format(
content, json.dumps(json_dict, sort_keys=True, indent=2, separators=(',', ': '))
)
view.show_popup(content, max_width=700, max_height=500)
class RequesterCancelRequestsCommand(sublime_plugin.WindowCommand):
"""Cancel unfinished requests in recently instantiated response pools.
"""
def run(self):
pools = RequestCommandMixin.RESPONSE_POOLS
while not pools.empty():
pool = pools.get()
if not pool.is_done:
pool.is_done = True
requests = pool.get_pending_requests()
if len(requests) > 100:
print('{} requests cancelled'.format(len(requests)))
return
for req in requests:
print('Request cancelled: {}'.format(req.request))
class RequesterResponseTabTogglePinnedCommand(sublime_plugin.WindowCommand):
"""Pin or unpin a response tab. A pinned response tab can't be overwritten.
"""
def run(self):
view = self.window.active_view()
if not view:
return
if not view.settings().get('requester.response_view', False):
return
if view.settings().get('requester.history_view', False):
return
pinned = bool(view.settings().get('requester.response_pinned', False))
pinned = not pinned
view.settings().set('requester.response_pinned', pinned)
set_response_view_name(view)
class RequesterReorderResponseTabsCommand(RequestsMixin, RequestCommandMixin, sublime_plugin.TextCommand):
"""Reorders open response tabs to match order of requests in current view.
"""
def get_requests(self):
try:
return parse_requests(self.view.substr(
sublime.Region(0, self.view.size())
))
except Exception as e:
sublime.error_message('Parse Error: there may be unbalanced parentheses in calls to requests')
print(e)
return []
def make_requests(self, requests, env):
requests = [prepare_request(request, env, i) for i, request in enumerate(requests)]
window = self.view.window()
# cache all response views in current window
response_views = []
for sheet in window.sheets():
view = sheet.view()
if view and view.settings().get('requester.response_view', False):
response_views.append(view)
View = namedtuple('View', 'view, ordering')
views = []
# add `ordering` property to cached response views, indicating at which ordering they appear in current view
for view in response_views:
method = view.settings().get('requester.request_method', None)
url = view.settings().get('requester.request_url', None)
if not method or not url:
views.append(View(view, maxsize))
continue
match = False
# see if request in response view matches any request parsed from requester file
for req in requests:
if not req.method or not req.url:
continue
if method == req.method and clean_url(url) == clean_url(req.url):
views.append(View(view, req.ordering))
match = True
break
if not match:
views.append(View(view, maxsize))
if not len(views):
return
# get largest index among open tabs
group, index = 0, 0
for sheet in window.sheets():
view = sheet.view()
group, index = window.get_view_index(view)
# sort response views by ordering property, and reorder response tabs
views.sort(key=lambda view: view.ordering)
# requester tab, then response tabs are moved sequentially to largest index
window.set_view_index(self.view, group, index)
for v in views:
window.set_view_index(v.view, group, index)
class RequesterSaveRequestCommand(sublime_plugin.WindowCommand):
"""Works only from response views, and not exploratory requests. Replaces
original request in requester file with modified request from respones tab, as
long as this original request hasn't changed in the meantime.
"""
def run(self):
view = self.window.active_view()
binding_info = view.settings().get('requester.binding_info', None)
if binding_info is None:
return
try:
request = parse_requests(view.substr(sublime.Region(0, view.size())), n=1)[0]
except Exception as e:
sublime.error_message('Save Error: there are no valid requests in your response view: {}'.format(e))
if request.startswith('requests.'):
request = request[len('requests.'):]
file, old_request = binding_info
if not os.path.isfile(file):
sublime.error_message('Save Error: requester file\n"{}"\nno longer exists'.format(file))
return
is_open = bool(self.window.find_open_file(file))
requester_view = self.window.open_file(file)
def save_request():
"""Wait on another thread for view to load on main thread, then save.
"""
for i in range(40): # don't wait more than 2s
if requester_view.is_loading():
time.sleep(.05)
else:
break
content = requester_view.substr(sublime.Region(0, requester_view.size()))
try:
start_index = content.index(old_request)
except ValueError as e:
sublime.error_message('Save Error: your original request was modified since you first sent it!')
return
# this is necessary for reasons due to undocumented behavior in ST API (probably a bug)
# simply calling `requester_view.replace` corrupts `view`s settings
requester_view.run_command(
'requester_replace_text',
{'text': request, 'start_index': start_index, 'end_index': start_index + len(old_request)})
requester_view.sel().clear()
requester_view.sel().add(sublime.Region(start_index))
if not is_open: # hacky trick to make sure scroll works
time.sleep(.2)
else:
time.sleep(.1)
requester_view.show_at_center(start_index)
set_save_info_on_view(view, request)
sublime.set_timeout_async(save_request, 0)
def set_save_info_on_view(view, request):
"""Set file name and request string on view.
"""
file = view.settings().get('requester.file', None)
if file is None:
return
if request.startswith('requests.'):
request = request[len('requests.'):]
view.settings().set('requester.binding_info', (file, request))
def set_request_on_view(view, res):
"""For reordering requests, showing pending activity for requests, and
jumping to matching response tabs after requests return.
"""
view.settings().set('requester.request_method', res.request.method)
view.settings().set('requester.request_url', res.url.split('?')[0])