├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── freud
├── __init__.py
├── __main__.py
├── api
│ ├── __init__.py
│ ├── request.py
│ └── response.py
├── key_bindings.py
├── model
│ └── __init__.py
├── server_control
│ ├── __init__.py
│ ├── auth_dialog.py
│ ├── call_editor.py
│ ├── delete_server.py
│ ├── headers_dialog.py
│ └── server_dialog.py
├── ui
│ ├── __init__.py
│ ├── body_container.py
│ ├── dialog.py
│ ├── keys.py
│ ├── root_container.py
│ ├── server_container.py
│ ├── sort.py
│ ├── style.py
│ └── text_buffers.py
└── utils.py
├── img
└── demo.gif
├── setup.py
├── tests
├── __init__.py
├── conftest.py
├── test_database.py
├── test_layout.py
├── test_requests.py
└── utils.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.egg-info
3 | *.db
4 | .venv/
5 | .tox/
6 | .eggs/
7 | config/*.ini
8 | .pytest_cache
9 | dist
10 | build
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.3 (unreleased)
2 |
3 | - No changes
4 |
5 | ## 0.1.2 (2018-08-09)
6 |
7 | - Add CHANGELOG.md
8 | - Install test dependencies through pip install -e '[dev]'
9 | - Install tox for python 3.5 and python 3.6
10 | - Add Makefile
11 |
12 | ## 0.1.1 (2018-08-08)
13 |
14 | - Initial release
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Stephen Martin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md
2 | include LICENSE
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: install install-dev test test-all flake8 distclean
2 |
3 | install:
4 | python setup.py install
5 |
6 | install-dev:
7 | pip install -q -e .[dev]
8 |
9 | test: install-dev
10 | pytest
11 |
12 | test-all: install-dev
13 | tox
14 |
15 | flake8: install-dev
16 | flake8 freud tests
17 |
18 | distclean:
19 | rm -fr *.egg *.egg-info/ dist/ build/
20 |
21 | publish:
22 | python setup.py sdist bdist_wheel
23 | twine upload dist/*
24 | distclean
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Freud is a TUI API endpoint analyzer utilizing Python Prompt Toolkit and
4 | Requests. It allows creating and saving request headers, authentication (basic
5 | and digest), and body using both integrated forms and your native
6 | editor.
7 |
8 | Currently, it is designed and tested for receiving JSON, XML, and HTML
9 | responses, but more can be added later as needed.
10 |
11 | ## Installation
12 |
13 | > You can install through PyPI...
14 |
15 | ```shell
16 | python -m venv .venv
17 | . .venv/bin/activate
18 | pip install freud
19 | ```
20 |
21 | > or from sources
22 |
23 | ```
24 | git clone https://github.com/stloma/freud
25 | cd freud
26 | python -m venv .venv
27 | . .venv/bin/activate
28 | python setup.py install
29 | ```
30 |
31 | ## Keys
32 |
33 | > Key shortcuts depend on which window you are in. There are 4 windows: server
34 | > list (left window), response headers (top right), response body (middle
35 | > right), and server summary (bottom).
36 |
37 | * Server list/left window
38 | - New server: `n`
39 | - Select server: `enter`
40 | - Edit selected server: `e`
41 | - Edit authentication: `a`
42 | - Edit body: `b`
43 | - Send request for selected server: `r`
44 | - Delete selected server: `d`
45 | - Sort servers: `s`
46 | - Top/bottom of server list: `gg/G`
47 |
48 | * Header window, Response body window, Server summary window
49 | - `h/j/k/l` Vi keybindings for movement
50 | - `/` Search text
51 | - `o`: Open response body in external editor
52 |
53 | * General Navigation
54 | - Quit: `Ctrl+c`
55 | - Key Binding Quick Reference: `Ctrl+f`
56 | - Next window: `Tab`
57 | - Previous window: `Shift+Tab`
58 |
59 |
60 | ## Advanced uses
61 |
62 | #### More keys
63 | * Copy/Paste: `Shift+Click`
64 |
65 | #### Changing default configuration
66 |
67 | * Settings file: `config/freud.ini`
68 |
69 | ## Roadmap
70 |
71 | Freud is still in development, but should work well for most use
72 | cases.
73 |
74 | Currently, it is designed to handle JSON, XML, and HTML responses; I
75 | haven't tested others. If you would like it to handle something specific, you
76 | can either submit a PR or create an issue and I'll add it!
77 |
78 | #### Goals
79 |
80 | * Add more authentication types (e.g., OAuth, Bearer Token, etc.)
81 | * Handle more Content-Types (MIME types)
82 | * Cookie handling
83 | * Add capability to organize requests under categories
84 | * Increase testing coverage
85 |
86 | ## Requirements
87 |
88 | * Python: 3.5+
89 | * Python Prompt Toolkit, Requests, Pygments
90 | * set \$EDITOR environment variable
91 | - bash/zsh: `export EDITOR=$(which vim)`
92 |
93 | ## Testing
94 |
95 | ```
96 | pip install -e '.[dev]'
97 | pytest
98 | ```
99 |
--------------------------------------------------------------------------------
/freud/__init__.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import os
3 | import sys
4 | import pkg_resources
5 |
6 | folder = 'config/'
7 |
8 | config_file = os.path.join(folder, 'freud.ini')
9 |
10 | __version__ = pkg_resources.get_distribution('freud').version
11 |
12 | DEFAULT_SETTINGS = '''
13 | [API]
14 | # Options: yes, no
15 | allow_redirects = yes
16 |
17 | [LAYOUT]
18 | # Width of sidebar that contains server names
19 | server_width = 15
20 | # Height of container that holds headers
21 | header_height = 10
22 | # Height of container that shows server summary
23 | summary_height = 10
24 |
25 | [KEYS]
26 | new_server = n
27 | edit_server = e
28 | send_request = r
29 | edit_authentication = a
30 | edit_headers = h
31 | edit_body = b
32 | delete_server = d
33 | open_response_body = o
34 | sort_servers = s
35 | key_quick_ref = c-f
36 |
37 | [DB]
38 | filename = requests.db
39 |
40 | [JSON]
41 | indentation = 2
42 |
43 | [SORT_BY]
44 | # Column options: name, timestamp, url, method, body, authtype, authuser,
45 | # authpass, headers
46 | # Order options: asc, desc
47 | column = timestamp
48 | order = asc
49 |
50 | [STYLE]
51 | # More styles here:
52 | # https://bitbucket.org/birkenfeld/pygments-main/src/stable/pygments/styles
53 | theme = freud
54 | separator_line_fg = gray
55 | # separator_line_bg = black
56 | '''
57 |
58 | if not os.path.exists(folder):
59 | os.makedirs(folder)
60 |
61 | if not os.path.exists(config_file):
62 | with open(config_file, 'w') as cfile:
63 | cfile.write(DEFAULT_SETTINGS)
64 |
65 | config = configparser.ConfigParser()
66 |
67 | config.read(config_file)
68 |
69 | if hasattr(sys, '_called_from_test'):
70 | config['DB']['filename'] = 'delete_freud_test_database.db'
71 |
72 | redirects = config['API']['allow_redirects']
73 | if redirects.lower() == 'no':
74 | ALLOW_REDIRECTS = False
75 | else:
76 | ALLOW_REDIRECTS = True
77 |
78 | KEYS = config['KEYS']
79 |
80 | SERVER_WIDTH = int(config['LAYOUT']['server_width'])
81 |
82 | HEADER_HEIGHT = int(config['LAYOUT']['header_height'])
83 |
84 | SUMMARY_HEIGHT = int(config['LAYOUT']['summary_height'])
85 |
86 | STYLE = config['STYLE']
87 |
88 | DB_FILE = config['DB']['filename']
89 |
90 | SORT_BY = config['SORT_BY']
91 |
92 | INDENTATION = int(config['JSON']['indentation'])
93 |
--------------------------------------------------------------------------------
/freud/__main__.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from prompt_toolkit.application import Application
4 | from prompt_toolkit.layout.layout import Layout
5 | from prompt_toolkit.enums import EditingMode
6 | from prompt_toolkit.eventloop import use_asyncio_event_loop
7 |
8 | from freud.key_bindings import kb
9 | from freud.ui.server_container import servers
10 | from freud.ui.root_container import root_container
11 | from freud.ui.style import style
12 | from freud.utils import on_startup
13 |
14 |
15 | use_asyncio_event_loop()
16 |
17 |
18 | def main():
19 | app = Application(
20 | layout=Layout(root_container.create(),
21 | focused_element=servers.content),
22 | key_bindings=kb,
23 | editing_mode=EditingMode.VI,
24 | style=style,
25 | mouse_support=True,
26 | full_screen=True,
27 | after_render=on_startup
28 | )
29 |
30 | asyncio.get_event_loop().run_until_complete(
31 | app.run_async().to_asyncio_future()
32 | )
33 |
--------------------------------------------------------------------------------
/freud/api/__init__.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from prompt_toolkit.filters.utils import to_filter
4 |
5 | from freud.ui.dialog import LoadingDialog, ErrorDialog
6 | from freud.utils import ButtonManager
7 | from freud.ui.text_buffers import (
8 | response_buffer, header_buffer)
9 | from freud.api.request import request_handler
10 |
11 |
12 | def send_request(event):
13 | """ Prepare and send request, then display the response """
14 |
15 | name = ButtonManager.current_button
16 |
17 | # Display loading dialog
18 | loading_dialog = LoadingDialog(
19 | event=event,
20 | title='Loading',
21 | text='Connecting to {}...'.format(name)
22 | )
23 |
24 | async def send_request_async():
25 | # Cede control of event loop. See:
26 | # github.com/python/asyncio/issues/284ield
27 | await asyncio.sleep(0)
28 |
29 | # Build and send request to server
30 | result = await request_handler(name)
31 |
32 | loading_dialog.close_dialog()
33 |
34 | errors = result.get('errors')
35 |
36 | if errors:
37 | error_type, error_value = list(errors.items())[0]
38 |
39 | header_buffer.read_only = to_filter(False)
40 | header_buffer.text = error_type
41 | header_buffer.read_only = to_filter(True)
42 |
43 | response_buffer.read_only = to_filter(False)
44 | response_buffer.text = ''
45 | response_buffer.read_only = to_filter(True)
46 |
47 | # LoadingDialog is not removed if called a second time. Invalidate
48 | # method sends a repaint trigger
49 | event.app.invalidate()
50 |
51 | return ErrorDialog(event, title=error_type,
52 | text=error_value)
53 |
54 | headers, response_body = result.get('response')
55 |
56 | header_buffer.read_only = to_filter(False)
57 | header_buffer.text = headers
58 | header_buffer.read_only = to_filter(True)
59 |
60 | response_buffer.read_only = to_filter(False)
61 | response_buffer.text = response_body
62 | response_buffer.read_only = to_filter(True)
63 |
64 | asyncio.ensure_future(send_request_async())
65 |
--------------------------------------------------------------------------------
/freud/api/request.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import json
3 |
4 | from freud.model import db
5 | from freud.api.response import response_handler
6 | from freud import ALLOW_REDIRECTS
7 |
8 |
9 | def session_builder(req):
10 | session = requests.Session()
11 | request = requests.Request(req.method, req.url)
12 |
13 | headers = {
14 | 'user-agent': 'Freud',
15 | 'accept-encoding': 'gzip, deflate',
16 | 'accept': '*/*',
17 | 'connection': 'keep-alive',
18 | 'charset': 'UTF-8',
19 | }
20 | request.headers = headers
21 |
22 | if req.auth:
23 |
24 | auth = json.loads(req.auth)
25 | authtype = auth.get('type')
26 | authuser = auth.get('user')
27 | authpass = auth.get('password')
28 |
29 | if authtype == 'basic':
30 | request.auth = (authuser, authpass)
31 | elif authtype == 'digest':
32 | request.auth = requests.auth.HTTPDigestAuth(
33 | authuser, authpass)
34 |
35 | if req.headers:
36 | headers = json.loads(req.headers)
37 | request.headers.update(headers)
38 |
39 | for header in request.headers:
40 | if header.lower() == 'content-type':
41 | request.content_type = request.headers[header]
42 |
43 | request.data = req.body
44 |
45 | response = None
46 | errors = None
47 |
48 | try:
49 | response = session.send(
50 | request.prepare(), allow_redirects=ALLOW_REDIRECTS)
51 |
52 | except requests.exceptions.ConnectionError as e:
53 | errors = {'Connection error': str(e)}
54 |
55 | except requests.exceptions.RequestException as e:
56 | errors = {'Request Exception': str(e)}
57 |
58 | return [response, errors]
59 |
60 |
61 | async def request_handler(name):
62 |
63 | # Get selected server url
64 | server = db.fetch_one(name=name)
65 |
66 | response, errors = session_builder(server)
67 |
68 | if errors:
69 | return {'errors': errors}
70 |
71 | headers, response_body = response_handler(response)
72 |
73 | return {'response': [headers, response_body]}
74 |
--------------------------------------------------------------------------------
/freud/api/response.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import json
3 | import xml
4 |
5 | from prompt_toolkit.lexers import PygmentsLexer
6 | from pygments.lexers import HtmlLexer, JsonLexer, XmlLexer
7 |
8 | from freud.ui.text_buffers import response_box
9 | from freud import INDENTATION
10 |
11 |
12 | def response_handler(response):
13 | # Format content for header_buffer
14 | #
15 | status_code_lookup = requests.status_codes._codes
16 | status_description = status_code_lookup[response.status_code][0]
17 | status_description = status_description[0].upper(
18 | ) + status_description[1:]
19 |
20 | seconds = response.elapsed.total_seconds()
21 | elapsed = str('{}ms'.format(round(seconds * 1000, 2)))
22 |
23 | # if elapsed time is 1 second or greater, display seconds instead of ms
24 | if (len(elapsed.split('.')[0])) > 3:
25 | elapsed = str('{}s'.format(round(seconds, 2)))
26 |
27 | status_code = ['{} {}'.format(
28 | response.status_code, status_description)]
29 |
30 | headers = response.headers
31 |
32 | headers_string = ''
33 | for k, v in headers.items():
34 | headers_string += '{}: {}\n'.format(k, v)
35 | headers_string = headers_string.rstrip('\n')
36 |
37 | headers = '{} | {}\n{}'.format(status_code, elapsed, headers_string)
38 |
39 | response_body = content_type_handler(response)
40 |
41 | # Format content for response_buffer
42 | #
43 | return headers, response_body
44 |
45 |
46 | def content_type_handler(response):
47 |
48 | content_type = response.headers.get('Content-Type')
49 | response_body = None
50 | if content_type:
51 |
52 | if content_type.startswith('application/json'):
53 | try:
54 | response_body = json.dumps(response.json(), indent=INDENTATION)
55 | response_box.buffer_control.lexer = PygmentsLexer(
56 | JsonLexer)
57 | except json.decoder.JSONDecodeError as e:
58 | response_body = 'JSON Error: {}\n\n'.format(str(e))
59 | response_body += response.text
60 |
61 | elif content_type.startswith('text/html'):
62 | response_body = response.text
63 | response_box.buffer_control.lexer = PygmentsLexer(
64 | HtmlLexer)
65 |
66 | elif content_type.startswith('text/xml'):
67 | try:
68 | xml_data = xml.dom.minidom.parseString(response.text)
69 | response_body = xml_data.toprettyxml(indent=' ' * 2)
70 | response_box.buffer_control.lexer = PygmentsLexer(
71 | XmlLexer)
72 | except xml.parsers.expat.ExpatError as e:
73 | response_body = 'XML Error: {}\n\n'.format(str(e))
74 | response_body += response.text
75 |
76 | if not response_body:
77 | response_body = response.text
78 |
79 | return response_body
80 |
--------------------------------------------------------------------------------
/freud/key_bindings.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.key_binding import KeyBindings
2 | from prompt_toolkit.filters import Condition
3 |
4 | from freud.server_control.delete_server import DeleteDialog
5 | from freud.server_control.headers_dialog import HeadersDialog
6 | from freud.server_control.server_dialog import ServerDialog
7 | from freud.utils import ButtonManager
8 | from freud.ui.text_buffers import (
9 | response_buffer, header_buffer, summary_buffer)
10 | from freud.server_control.call_editor import (
11 | update_body, open_response_in_editor)
12 | from freud.server_control.auth_dialog import AuthSelector
13 | from freud.ui.keys import KeyQuickRef
14 | from freud.ui.sort import SortDialog
15 | from freud.api import send_request
16 | from freud import KEYS
17 |
18 |
19 | kb = KeyBindings()
20 | server_kb = KeyBindings()
21 | response_kb = KeyBindings()
22 | header_kb = KeyBindings()
23 | summary_kb = KeyBindings()
24 | completion = KeyBindings()
25 |
26 |
27 | @Condition
28 | def is_button():
29 | """ Filter when there are no buttons """
30 |
31 | return len(ButtonManager.buttons) > 0
32 |
33 |
34 | # Global Keys
35 | #
36 |
37 | @kb.add('c-c', eager=True)
38 | def quit_app(event):
39 | event.app.exit()
40 |
41 |
42 | @server_kb.add('c-f')
43 | @response_kb.add('c-f')
44 | def keys_quick_reference(event):
45 | KeyQuickRef(event)
46 |
47 |
48 | # Navigation Keys
49 | #
50 |
51 | @server_kb.add('tab', filter=is_button)
52 | def go_to_header_from_server(event):
53 | event.app.layout.focus(header_buffer)
54 |
55 |
56 | @server_kb.add('s-tab', filter=is_button)
57 | def go_to_summary_from_servers(event):
58 | event.app.layout.focus(summary_buffer)
59 |
60 |
61 | @header_kb.add('tab', filter=is_button)
62 | def go_to_response_from_header(event):
63 | event.app.layout.focus(response_buffer)
64 |
65 |
66 | @header_kb.add('s-tab', filter=is_button)
67 | def go_to_server_from_header(event):
68 | event.app.layout.focus(ButtonManager.prev_button)
69 |
70 |
71 | @response_kb.add('tab', filter=is_button)
72 | def go_to_summary_from_response(event):
73 | event.app.layout.focus(summary_buffer)
74 |
75 |
76 | @response_kb.add('s-tab', filter=is_button)
77 | def go_to_headers_from_resposne(event):
78 | event.app.layout.focus(header_buffer)
79 |
80 |
81 | @summary_kb.add('tab')
82 | def go_to_servers_from_summary(event, filter=is_button):
83 | event.app.layout.focus(ButtonManager.prev_button)
84 |
85 |
86 | @summary_kb.add('s-tab', filter=is_button)
87 | def go_to_response_from_summary(event):
88 | event.app.layout.focus(response_buffer)
89 |
90 |
91 | @response_kb.add('h', filter=is_button)
92 | @response_kb.add('left', filter=is_button)
93 | def go_to_servers(event):
94 | if (len(ButtonManager.buttons) > 0):
95 | event.app.layout.focus(ButtonManager.prev_button)
96 |
97 |
98 | @server_kb.add('l', filter=is_button)
99 | @server_kb.add('right', filter=is_button)
100 | def go_to_response(event):
101 | event.app.layout.focus(response_buffer)
102 |
103 |
104 | @server_kb.add('g', filter=is_button)
105 | def top_of_list(event):
106 | """ Adds vi binding to jump to top of server list """
107 |
108 | buttons = ButtonManager.buttons
109 |
110 | event.app.layout.focus(buttons[0])
111 | ButtonManager.prev_button = buttons[0]
112 |
113 |
114 | @server_kb.add('G', filter=is_button)
115 | def bottom_of_list(event):
116 | """ Adds vi binding to jump to bottom of server list """
117 |
118 | buttons = ButtonManager.buttons
119 |
120 | event.app.layout.focus(buttons[-1])
121 | ButtonManager.prev_button = buttons[-1]
122 |
123 |
124 | @server_kb.add('j', filter=is_button)
125 | @server_kb.add('down')
126 | def down_button(event):
127 | """ Scroll down list of servers in the server_container """
128 |
129 | buttons = ButtonManager.buttons
130 |
131 | current_window = event.app.layout.current_window
132 | idx = buttons.index(current_window)
133 |
134 | try:
135 | event.app.layout.focus(buttons[idx + 1])
136 | ButtonManager.prev_button = buttons[idx + 1]
137 | except IndexError:
138 | # If we're at the button item, loop to first item
139 |
140 | event.app.layout.focus(buttons[0])
141 | ButtonManager.prev_button = buttons[0]
142 |
143 |
144 | @server_kb.add('k', filter=is_button)
145 | @server_kb.add('up')
146 | def up_button(event):
147 | """ Scroll up list of servers in the server_container """
148 |
149 | buttons = ButtonManager.buttons
150 |
151 | current_window = event.app.layout.current_window
152 | idx = buttons.index(current_window)
153 |
154 | try:
155 | event.app.layout.focus(buttons[idx - 1])
156 | ButtonManager.prev_button = buttons[idx - 1]
157 | except IndexError:
158 | # If we're at the top item, loop to last item
159 |
160 | event.app.layout.focus(buttons[-1])
161 | ButtonManager.prev_button = buttons[-1]
162 |
163 |
164 | # Server control keys
165 | #
166 | @server_kb.add(KEYS['open_response_body'], filter=is_button)
167 | @response_kb.add(KEYS['open_response_body'], filter=is_button)
168 | def open_response(event):
169 | open_response_in_editor(event)
170 |
171 |
172 | @server_kb.add(KEYS['sort_servers'], filter=is_button)
173 | def sort(event):
174 | SortDialog(event)
175 |
176 |
177 | @server_kb.add(KEYS['new_server'])
178 | def new_server(event):
179 | ServerDialog(event, create_server=True)
180 |
181 |
182 | @server_kb.add(KEYS['edit_headers'], filter=is_button)
183 | def new_headers(event):
184 | HeadersDialog(event)
185 |
186 |
187 | @server_kb.add(KEYS['edit_server'], filter=is_button)
188 | def edit(event):
189 | ServerDialog(event)
190 |
191 |
192 | @server_kb.add(KEYS['edit_authentication'], filter=is_button)
193 | def edit_authentication(event):
194 | AuthSelector(event)
195 |
196 |
197 | @server_kb.add(KEYS['edit_body'], filter=is_button)
198 | def edit_body(event):
199 | update_body(event)
200 |
201 |
202 | @server_kb.add(KEYS['delete_server'], filter=is_button)
203 | def rm_server(event):
204 | DeleteDialog(event)
205 |
206 |
207 | @server_kb.add(KEYS['send_request'], filter=is_button)
208 | def request(event):
209 | send_request(event)
210 |
--------------------------------------------------------------------------------
/freud/model/__init__.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 | import os
3 | import datetime
4 | from collections import namedtuple
5 |
6 | from freud import DB_FILE, SORT_BY
7 |
8 | basedir = os.path.abspath(os.path.dirname(__file__))
9 |
10 |
11 | class Db:
12 |
13 | def __init__(self, database=DB_FILE):
14 |
15 | self.database = database
16 | self.connect()
17 | self.close()
18 |
19 | def namedtuple_factory(self, cursor, row):
20 | """ Returns namedtuple results, named with column names """
21 |
22 | fields = [col[0] for col in cursor.description]
23 | Row = namedtuple('Row', fields)
24 | return Row(*row)
25 |
26 | def connect(self):
27 |
28 | self.conn = sqlite3.connect(
29 | os.path.join(basedir, self.database),
30 | detect_types=sqlite3.PARSE_COLNAMES
31 | )
32 | self.conn.row_factory = self.namedtuple_factory
33 | self.cursor = self.conn.cursor()
34 |
35 | self.cursor.execute('''CREATE TABLE IF NOT EXISTS requests (
36 | name text not null unique,
37 | timestamp timestamp,
38 | url text not null,
39 | method text not null,
40 | body text,
41 | auth text,
42 | headers text
43 | )
44 | ''')
45 |
46 | def close(self):
47 |
48 | self.conn.commit()
49 | self.conn.close()
50 |
51 | def fetch_all(self, sort_by=None, order=None):
52 | """ Returns all rows """
53 |
54 | sort_by = sort_by if sort_by else SORT_BY['column']
55 | order = order if order else SORT_BY['order']
56 |
57 | self.connect()
58 |
59 | if sort_by and order:
60 | self.cursor.execute(
61 | 'SELECT * FROM requests ORDER BY {} {}'.format(sort_by, order))
62 | elif sort_by:
63 | self.cursor.execute(
64 | 'SELECT * FROM requests ORDER BY {sort_by}'.format(sort_by))
65 | else:
66 | self.cursor.execute('SELECT * FROM requests')
67 |
68 | rows = self.cursor.fetchall()
69 |
70 | self.close()
71 |
72 | return rows
73 |
74 | def fetch_one(self, rowid=None, name=None):
75 |
76 | self.connect()
77 |
78 | if rowid:
79 | self.cursor.execute('''SELECT * FROM requests WHERE rowid = ?''',
80 | (rowid,))
81 | else:
82 | self.cursor.execute('''
83 | SELECT rowid, * FROM requests WHERE name = ?''',
84 | (name,))
85 |
86 | result = self.cursor.fetchone()
87 |
88 | if result:
89 | return result
90 |
91 | return False
92 |
93 | self.close()
94 |
95 | def add_one(self, values):
96 |
97 | self.connect()
98 |
99 | try:
100 | with self.conn:
101 | self.conn.execute('''INSERT INTO requests
102 | (
103 | name, url, method, timestamp, auth,
104 | body, headers)
105 | VALUES (?, ?, ?, ?, ?, ?, ?)''',
106 | (
107 | values['name'],
108 | values['url'],
109 | values['method'],
110 | datetime.datetime.now(),
111 | values.get('auth'),
112 | values.get('body'),
113 | values.get('headers')
114 | ))
115 | except sqlite3.IntegrityError as e:
116 | return {'errors': 'sqlite error: {}'.format(e.args[0])}
117 |
118 | except KeyError as e:
119 | return {'errors': 'missing column error: {}'.format(e.args[0])}
120 |
121 | finally:
122 | self.close()
123 |
124 | return {'success': True}
125 |
126 | def delete_one(self, name):
127 |
128 | self.connect()
129 | self.cursor.execute('''DELETE FROM requests WHERE name = ?''',
130 | (name,))
131 |
132 | self.close()
133 |
134 | def delete_all(self):
135 | """ Used for testing """
136 |
137 | self.connect()
138 | self.cursor.execute('''DELETE FROM requests''')
139 |
140 | self.close()
141 |
142 | def update_one(self, values=None, rowid=None):
143 | """ Updates one row by rowid if supplied, else by name """
144 |
145 | self.connect()
146 |
147 | if rowid:
148 | # If the rowid is set, we could be changing the name
149 | name = values.get('name')
150 | url = values.get('url')
151 | method = values.get('method')
152 |
153 | if not all([name, url, method]):
154 | return {'errors': 'Name, url, and method are required'}
155 |
156 | try:
157 | with self.conn:
158 | self.conn.execute('''UPDATE requests SET
159 | url=?, method=?, name=? where rowid=?''',
160 | (url, method, name, rowid))
161 |
162 | except sqlite3.IntegrityError as e:
163 | return {'errors': 'sqlite error: {}'.format(e.args[0])}
164 |
165 | except KeyError as e:
166 | return {
167 | 'errors': 'missing column error: {}'.format(e.args[0])}
168 |
169 | finally:
170 | self.close()
171 |
172 | else:
173 | # Changes all fields passed in through values
174 | name = values.get('name')
175 |
176 | result = self.fetch_one(name=name)
177 |
178 | body = values.get('body', result.body)
179 | headers = values.get('headers', result.headers)
180 | auth = values.get('auth', result.auth)
181 |
182 | self.connect()
183 |
184 | try:
185 |
186 | with self.conn:
187 | self.conn.execute('''
188 | UPDATE requests SET
189 | body=?, headers=?, auth=?
190 | where name=?''',
191 | (body, headers, auth,
192 | name
193 | )
194 | )
195 |
196 | except sqlite3.IntegrityError as e:
197 | return {'errors': 'sqlite error: {}'.format(e.args[0])}
198 |
199 | except KeyError as e:
200 | return {'errors': 'missing column error: {}'.format(e.args[0])}
201 |
202 | finally:
203 | self.close()
204 |
205 | return {'success': True}
206 |
207 |
208 | db = Db()
209 |
--------------------------------------------------------------------------------
/freud/server_control/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stloma/freud/ebc16ecf7bd39ad365108e1cce4c9e8a0ddc69cd/freud/server_control/__init__.py
--------------------------------------------------------------------------------
/freud/server_control/auth_dialog.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from prompt_toolkit.layout.dimension import D
4 | from prompt_toolkit.document import Document
5 | from prompt_toolkit.layout import Float
6 | from prompt_toolkit.widgets import Button, TextArea, Dialog, Label
7 | from prompt_toolkit.layout.containers import HSplit, Window
8 | from prompt_toolkit.widgets import RadioList
9 | from prompt_toolkit.key_binding import KeyBindings
10 |
11 | from freud.ui.root_container import root_container
12 | from freud.ui.dialog import ErrorDialog
13 | from freud.model import db
14 | from freud.utils import ButtonManager, select_item
15 |
16 |
17 | class DeleteConfirmation:
18 | """ Confirm deletion of authentication information """
19 |
20 | def __init__(self, event, dialog):
21 |
22 | def ok_handler():
23 | root_container.floats.pop()
24 | db.update_one(values={'name': name,
25 | 'auth': None})
26 | event.app.layout.focus(ButtonManager.prev_button)
27 | select_item(event)
28 |
29 | def cancel_handler():
30 | root_container.floats.pop()
31 | root_container.floats.append(self.auth_float)
32 | event.app.layout.focus(dialog)
33 |
34 | ok_button = Button(text='OK', handler=ok_handler)
35 | cancel_button = Button(text='Cancel', handler=cancel_handler)
36 |
37 | name = ButtonManager.current_button
38 |
39 | self.dialog = Dialog(
40 | title='Delete confirmation',
41 | body=Label(
42 | text='Are you sure you want to delete authentication for {}?'
43 | .format(name)
44 | ),
45 | buttons=[cancel_button, ok_button],
46 | width=D(preferred=80),
47 | with_background=True
48 | )
49 |
50 | self.auth_float = root_container.floats.pop()
51 | root_container.floats.append(Float(self.dialog))
52 | event.app.layout.focus(self.dialog)
53 |
54 |
55 | class AuthSelector:
56 | """ Provide selection of authentication types. If the server already has
57 | authentication, skip selecting and open the respective dialog box """
58 |
59 | def __init__(self, event):
60 |
61 | def dialog_opener(authtype, auth={}):
62 |
63 | if authtype == 'basic':
64 | BasicAuthDialog(event, auth)
65 | elif authtype == 'digest':
66 | DigestAuthDialog(event, auth)
67 |
68 | def ok_handler():
69 |
70 | root_container.floats.pop()
71 |
72 | authtype = self.radio_list.current_value['authtype']
73 | dialog_opener(authtype)
74 |
75 | def cancel_handler():
76 | root_container.floats.pop()
77 | root_container.float_container.key_bindings = None
78 |
79 | event.app.layout.focus(ButtonManager.prev_button)
80 |
81 | kb = KeyBindings()
82 |
83 | server = db.fetch_one(name=ButtonManager.current_button)
84 |
85 | if server.auth:
86 |
87 | auth = json.loads(server.auth)
88 | auth_type = auth.get('type')
89 | dialog_opener(auth_type, auth=auth)
90 |
91 | else:
92 |
93 | ok_button = Button(text='OK', handler=ok_handler)
94 | cancel_button = Button(text='Cancel', handler=cancel_handler)
95 |
96 | self.radio_list = RadioList(values=[
97 | ({'authtype': 'basic'}, 'Basic'),
98 | ({'authtype': 'digest'}, 'Digest')
99 | ])
100 |
101 | kb = self.radio_list.control.key_bindings
102 |
103 | @kb.add('j')
104 | def down(event):
105 | self.radio_list._selected_index = min(
106 | len(self.radio_list.values) - 1,
107 | self.radio_list._selected_index + 1
108 | )
109 |
110 | @kb.add('k')
111 | def up(event):
112 | self.radio_list._selected_index = max(
113 | 0, self.radio_list._selected_index - 1)
114 |
115 | @kb.add('g', 'g')
116 | def top(event):
117 | self.radio_list._selected_index = 0
118 |
119 | @kb.add('G')
120 | def bottom(event):
121 | self.radio_list._selected_index = len(
122 | self.radio_list.values) - 1
123 |
124 | self.dialog = Dialog(
125 | title='Select auth type',
126 | body=self.radio_list,
127 | buttons=[ok_button, cancel_button],
128 | width=D(preferred=80),
129 | with_background=True,
130 | modal=True)
131 |
132 | root_container.float_container.key_bindings = kb
133 |
134 | root_container.floats.append(Float(self.dialog))
135 | event.app.layout.focus(self.dialog)
136 |
137 |
138 | class AuthDialog:
139 | """ Parent dialog for authentication """
140 |
141 | def __init__(self):
142 |
143 | self.name = ButtonManager.current_button
144 |
145 | self.ok_button = Button(text='OK', handler=self.ok_handler)
146 |
147 | self.cancel_button = Button(
148 | text='Cancel', handler=self.cancel_handler)
149 |
150 | self.delete_button = Button(
151 | text='Delete', handler=self.delete_handler)
152 |
153 | self.authuser = TextArea(
154 | multiline=False
155 | )
156 |
157 | self.authpass_one = TextArea(
158 | multiline=False,
159 | password=True
160 | )
161 |
162 | self.authpass_two = TextArea(
163 | multiline=False,
164 | password=True
165 | )
166 |
167 | def ok_handler(self):
168 | authuser = self.authuser.text
169 | authpass = self.authpass_one.text
170 |
171 | all_fields = all([authuser, authpass])
172 | empty_fields = not any([authuser, authpass])
173 |
174 | if authpass != self.authpass_two.text:
175 | ErrorDialog(self.event, title='Password Error',
176 | text='Passwords do not match, please try again')
177 |
178 | elif all_fields or empty_fields:
179 |
180 | if all_fields:
181 | result = db.update_one({
182 | 'name': self.name,
183 | 'auth': json.dumps({
184 | 'user': authuser,
185 | 'password': authpass,
186 | 'type': self.authtype
187 | })
188 | })
189 |
190 | elif empty_fields:
191 | result = db.update_one(values={'name': self.name,
192 | 'auth': None})
193 |
194 | if result.get('success'):
195 | root_container.floats.pop()
196 | self.event.app.layout.focus(ButtonManager.prev_button)
197 | select_item(self.event)
198 |
199 | else:
200 | ErrorDialog(self.event, title='Add/edit server error',
201 | text=result.get('errors'))
202 |
203 | else:
204 | ErrorDialog(self.event, title='Input Error',
205 | text='Missing one or more fields.')
206 |
207 | def delete_handler(self):
208 | DeleteConfirmation(self.event, self.dialog)
209 |
210 | def cancel_handler(self):
211 | root_container.floats.pop()
212 | self.event.app.layout.focus(ButtonManager.prev_button)
213 |
214 |
215 | class BasicAuthDialog(AuthDialog):
216 | def __init__(self, event, auth):
217 |
218 | super().__init__()
219 |
220 | self.event = event
221 | self.authtype = 'basic'
222 |
223 | authuser = auth.get('user', '')
224 | authpass = auth.get('password', '')
225 |
226 | self.authuser.text = authuser
227 | self.authuser.buffer.document = Document(authuser, len(authuser))
228 |
229 | self.authpass_one.text = authpass
230 | self.authpass_one.buffer.document = Document(authpass, len(authpass))
231 |
232 | self.authpass_two.text = authpass
233 | self.authpass_two.buffer.document = Document(authpass, len(authpass))
234 |
235 | self.dialog = Dialog(
236 | title='Basic Authentication',
237 | body=HSplit([
238 | Label(text='Username:\n'),
239 | self.authuser,
240 | Window(height=1, char=' '),
241 | Label(text='Password'),
242 | self.authpass_one,
243 | Window(height=1, char=' '),
244 | Label(text='Retype password'),
245 | self.authpass_two
246 | ]),
247 | buttons=[self.ok_button, self.cancel_button, self.delete_button],
248 | width=D(preferred=80),
249 | with_background=True,
250 | modal=True)
251 |
252 | root_container.floats.append(Float(self.dialog))
253 | event.app.layout.focus(self.dialog)
254 |
255 |
256 | class DigestAuthDialog(AuthDialog):
257 | def __init__(self, event, auth):
258 |
259 | super().__init__()
260 |
261 | self.event = event
262 | self.authtype = 'digest'
263 |
264 | authuser = auth.get('user', '')
265 | authpass = auth.get('password', '')
266 |
267 | self.authuser.text = authuser
268 | self.authuser.buffer.document = Document(authuser, len(authuser))
269 |
270 | self.authpass_one.text = authpass
271 | self.authpass_one.buffer.document = Document(authpass, len(authpass))
272 |
273 | self.authpass_two.text = authpass
274 | self.authpass_two.buffer.document = Document(authpass, len(authpass))
275 |
276 | self.dialog = Dialog(
277 | title='Digest Authentication',
278 | body=HSplit([
279 | Label(text='Username:\n'),
280 | self.authuser,
281 | Window(height=1, char=' '),
282 | Label(text='Password'),
283 | self.authpass_one,
284 | Window(height=1, char=' '),
285 | Label(text='Retype password'),
286 | self.authpass_two
287 | ]),
288 | buttons=[self.ok_button, self.cancel_button, self.delete_button],
289 | width=D(preferred=80),
290 | with_background=True,
291 | modal=True)
292 |
293 | root_container.floats.append(Float(self.dialog))
294 | event.app.layout.focus(self.dialog)
295 |
--------------------------------------------------------------------------------
/freud/server_control/call_editor.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | from subprocess import call
3 | import os
4 |
5 | from freud.utils import select_item
6 | from freud.ui.text_buffers import response_buffer, header_buffer
7 | from freud.ui.dialog import ErrorDialog
8 | from freud.model import db
9 | from freud.utils import ButtonManager
10 |
11 |
12 | def update_body(event):
13 | """Uses external editor to create/update request body."""
14 |
15 | EDITOR = os.environ.get('EDITOR', None)
16 |
17 | if not EDITOR:
18 | return ErrorDialog(
19 | event, title='Error',
20 | text='Please set your $EDITOR environement variable'
21 | )
22 |
23 | tf = tempfile.NamedTemporaryFile()
24 |
25 | name = ButtonManager.current_button
26 |
27 | result = db.fetch_one(name=name)
28 |
29 | if result.body:
30 | with open(tf.name, 'w') as fout:
31 | fout.write(result.body)
32 |
33 | j = "+'set ft=json"
34 |
35 | call([EDITOR, j, tf.name])
36 |
37 | with open(tf.name, 'r') as fin:
38 | body = fin.read()
39 |
40 | db.update_one(values={'name': name,
41 | 'body': body})
42 |
43 | event.app.reset()
44 | select_item(event)
45 |
46 |
47 | def open_response_in_editor(event):
48 |
49 | EDITOR = os.environ.get('EDITOR', None)
50 |
51 | if not EDITOR:
52 | return ErrorDialog(
53 | event, title='Error',
54 | text='Please set your $EDITOR environement variable'
55 | )
56 |
57 | tf = tempfile.NamedTemporaryFile()
58 |
59 | headers = header_buffer.text
60 | response = response_buffer.text
61 |
62 | set_filetype = None
63 | if 'content-type: application/json' in headers.lower():
64 | set_filetype = 'json'
65 | elif 'text/html' in headers.lower():
66 | set_filetype = 'html'
67 |
68 | with open(tf.name, 'w') as fout:
69 | fout.write(str(response))
70 |
71 | if set_filetype:
72 | call([EDITOR, '+set ft={}'.format(set_filetype), tf.name])
73 | else:
74 | call([EDITOR, tf.name])
75 |
76 | event.app.reset()
77 |
--------------------------------------------------------------------------------
/freud/server_control/delete_server.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout import Float
2 | from prompt_toolkit.layout.dimension import D
3 | from prompt_toolkit.widgets import Button, Dialog, Label
4 | from prompt_toolkit.layout.containers import Window
5 | from prompt_toolkit.layout.controls import FormattedTextControl
6 | from prompt_toolkit.filters import to_filter
7 |
8 | from freud.model import db
9 | from freud.ui.root_container import root_container
10 | from freud.utils import ButtonManager, select_item
11 | from freud.ui.server_container import servers
12 | from freud.ui.text_buffers import summary_buffer
13 |
14 |
15 | class DeleteDialog:
16 | def __init__(self, event):
17 |
18 | def ok_handler():
19 | # if len(ButtonManager.buttons) > 0:
20 | delete_server(event, name)
21 | root_container.floats.pop()
22 |
23 | def cancel_handler():
24 | root_container.floats.pop()
25 | event.app.layout.focus(ButtonManager.prev_button)
26 |
27 | # Get data about server currently editing
28 | name = ButtonManager.current_button
29 |
30 | # Dialog configuration
31 | ok_button = Button(text='OK', handler=ok_handler)
32 | cancel_button = Button(text='Cancel', handler=cancel_handler)
33 |
34 | self.dialog = Dialog(
35 | title='Delete confirmation',
36 | body=Label(
37 | text='Are you sure you want to delete {}?'.format(name)),
38 | buttons=[cancel_button, ok_button],
39 | width=D(preferred=80),
40 | with_background=True
41 | )
42 |
43 | root_container.floats.append(Float(self.dialog))
44 | event.app.layout.focus(self.dialog)
45 |
46 |
47 | def delete_server(event, name):
48 |
49 | db.delete_one(name)
50 |
51 | buttons = servers.hsplit.children
52 |
53 | # The server has been removed from the db. Now, we have to remove the
54 | # button from the layout by iterating over buttons and removing it by index
55 | for idx, button in enumerate(buttons):
56 | if name == button.content.text()[1][1].strip():
57 |
58 | del buttons[idx]
59 |
60 | if len(buttons) > 0:
61 |
62 | try:
63 | # If the deleted button was not the last button
64 | event.app.layout.focus(buttons[idx])
65 | ButtonManager.prev_button = buttons[idx]
66 | select_item(event)
67 |
68 | except IndexError:
69 | # If the deleted button was the last button
70 | event.app.layout.focus(buttons[idx - 1])
71 | ButtonManager.prev_button = buttons[idx - 1]
72 | select_item(event)
73 |
74 | else:
75 | # Last button was deleted, display message "No servers"
76 |
77 | control = FormattedTextControl(
78 | 'No Servers',
79 | focusable=True,
80 | show_cursor=False)
81 |
82 | window = Window(
83 | control,
84 | height=1,
85 | dont_extend_width=True,
86 | dont_extend_height=True)
87 |
88 | buttons.append(window)
89 |
90 | summary_buffer.read_only = to_filter(False)
91 | summary_buffer.text = ''
92 | summary_buffer.read_only = to_filter(True)
93 |
94 | ButtonManager.prev_button = None
95 | ButtonManager.current_button = None
96 |
97 | event.app.layout.focus(servers.content)
98 |
--------------------------------------------------------------------------------
/freud/server_control/headers_dialog.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from prompt_toolkit.layout.dimension import D
4 | from prompt_toolkit.completion import WordCompleter
5 | from prompt_toolkit.widgets import Button, TextArea, Dialog, Label
6 | from prompt_toolkit.layout.containers import VSplit, HSplit, Window
7 | from prompt_toolkit.layout import Float
8 | from prompt_toolkit.key_binding import KeyBindings
9 |
10 |
11 | from freud.ui.dialog import ErrorDialog
12 | from freud.ui.root_container import root_container
13 | from freud.model import db
14 | from freud.utils import ButtonManager, select_item
15 |
16 |
17 | headers = [
18 | 'Accept',
19 | 'Accept-Charset',
20 | 'Accept-Encoding',
21 | 'Accept-Language',
22 | 'Accept-Datetime',
23 | 'Authorization',
24 | 'Cache-Control',
25 | 'Connection',
26 | 'Cookie',
27 | 'Content-Length',
28 | 'Content-MD5',
29 | 'Content-Type',
30 | 'Date',
31 | 'Expect',
32 | 'Forwarded',
33 | 'From',
34 | 'Host',
35 | 'If-Match',
36 | 'If-Modified-Since',
37 | 'If-None-Match',
38 | 'If-Range',
39 | 'If-Unmodified-Since',
40 | 'Max-Forwards',
41 | 'Origin',
42 | 'Pragma',
43 | 'Proxy-Authorization',
44 | 'Range',
45 | 'Referer',
46 | 'TE',
47 | 'User-Agent',
48 | 'Upgrade',
49 | 'Via',
50 | 'Warning',
51 | ]
52 |
53 | header_values = [
54 | 'application/json',
55 | 'text/html'
56 | ]
57 |
58 | headers_completer = WordCompleter(headers,
59 | ignore_case=True)
60 |
61 | header_values_completer = WordCompleter(header_values,
62 | ignore_case=True)
63 |
64 |
65 | class HeadersDialog:
66 | def __init__(self, event):
67 |
68 | def ok_handler():
69 | result = add_headers_to_db(event, name)
70 |
71 | if result.get('success'):
72 | root_container.floats.pop()
73 | event.app.layout.focus(ButtonManager.prev_button)
74 | select_item(event)
75 |
76 | def cancel_handler():
77 | root_container.floats.pop()
78 | event.app.layout.focus(ButtonManager.prev_button)
79 |
80 | # Get data about server currently editing
81 | name = ButtonManager.current_button
82 | result = db.fetch_one(name=name)
83 |
84 | # Dialog configuration
85 | ok_button = Button(text='OK', handler=ok_handler)
86 | cancel_button = Button(text='Cancel', handler=cancel_handler)
87 |
88 | local_kb = KeyBindings()
89 |
90 | @local_kb.add('enter')
91 | def cancel_completion(event):
92 | buff = event.app.current_buffer
93 | buff.complete_state = None
94 | event.app.layout.focus_next()
95 |
96 | @local_kb.add('c-n')
97 | def add_header(event):
98 | new_header = HeaderFactory()
99 | spacer = Window(height=1, char=' ')
100 | self.input.children.append(spacer)
101 | self.input.children.append(new_header.vsplit)
102 |
103 | # Setup initial headings for headers
104 | self.input = HSplit([
105 | Label(text='Press ctrl+n to add another header'),
106 | Window(height=1, char='─'),
107 | VSplit([
108 | Label(text='Header'),
109 | Window(width=1, char=' '),
110 | Label(text='Type'),
111 | ]),
112 | Window(height=1, char=' ')
113 | ], key_bindings=local_kb)
114 |
115 | # If there are existing headers, include them in the layout
116 | if result.headers:
117 | headers = json.loads(result.headers)
118 | for header in headers:
119 | new_header = HeaderFactory(
120 | header=header, value=headers[header])
121 | spacer = Window(height=1, char=' ')
122 | self.input.children.append(spacer)
123 | self.input.children.append(new_header.vsplit)
124 |
125 | # If there are no headers currently assigned to the server, create
126 | # empty boxes for input
127 | else:
128 | new_header = HeaderFactory()
129 | spacer = Window(height=1, char=' ')
130 | self.input.children.append(spacer)
131 | self.input.children.append(new_header.vsplit)
132 |
133 | self.dialog = Dialog(
134 | title='Headers',
135 | body=self.input,
136 | buttons=[ok_button, cancel_button],
137 | width=D(preferred=80),
138 | with_background=True
139 | )
140 |
141 | root_container.floats.append(Float(self.dialog))
142 | event.app.layout.focus(self.dialog)
143 |
144 |
145 | class HeaderFactory:
146 |
147 | headers = []
148 |
149 | def __init__(self, header=None, value=None):
150 |
151 | header = header if header else ''
152 | value = value if value else ''
153 |
154 | self.header = TextArea(
155 | multiline=False,
156 | password=False,
157 | completer=headers_completer,
158 | text=header
159 | )
160 | self.value = TextArea(
161 | multiline=False,
162 | password=False,
163 | completer=header_values_completer,
164 | text=value
165 | )
166 | self.vsplit = VSplit([
167 | self.header,
168 | Window(width=1, char=' '),
169 | self.value,
170 | ])
171 | HeaderFactory.headers.append(self)
172 |
173 |
174 | def add_headers_to_db(event, name):
175 |
176 | headers = {}
177 | for header in HeaderFactory.headers:
178 | if header.header.text == '' and header.value.text == '':
179 | # Skip empty input boxes
180 | continue
181 |
182 | if headers.get(header.header.text, None):
183 |
184 | ErrorDialog(event, title='Duplicate Header',
185 | text='Please remove duplicate header')
186 |
187 | return {'success': False}
188 |
189 | headers[header.header.text] = header.value.text
190 |
191 | if len(headers) == 0:
192 | # Since there are no headers, set headers to None in db
193 | result = db.update_one(values={'name': name,
194 | 'headers': None})
195 | else:
196 | result = db.update_one(values={'name': name,
197 | 'headers': json.dumps(headers)})
198 |
199 | # Delete all headers from factory
200 | HeaderFactory.headers = []
201 |
202 | if result.get('errors'):
203 | ErrorDialog(event, title='Header error',
204 | text=result.get('errors'))
205 |
206 | return {'success': False}
207 |
208 | return {'success': True}
209 |
--------------------------------------------------------------------------------
/freud/server_control/server_dialog.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout.dimension import D
2 | from prompt_toolkit.key_binding import KeyBindings
3 | from prompt_toolkit.completion import WordCompleter
4 | from prompt_toolkit.document import Document
5 | from prompt_toolkit.layout import Layout
6 | from prompt_toolkit.widgets import TextArea, Dialog, Label, Button
7 | from prompt_toolkit.layout.containers import HSplit, Window
8 | from prompt_toolkit.layout import Float
9 |
10 | from freud.ui.root_container import root_container
11 | from freud.model import db
12 | from freud.utils import ButtonManager, select_item
13 | from freud.ui.dialog import ErrorDialog
14 |
15 |
16 | class ServerDialog:
17 | def __init__(self, event, create_server=False):
18 |
19 | def ok_handler():
20 |
21 | # Simplistic check to see if user forgot to enter something for url
22 | url = len(self.url.text) > len('https://')
23 |
24 | if not all([self.name.text, url, self.method.text]):
25 | return ErrorDialog(event, title='Input Error',
26 | text='Name, Url, and Method are required.')
27 |
28 | result = server_to_db()
29 |
30 | if result.get('success'):
31 |
32 | root_container.floats.pop()
33 |
34 | # Rather than inserting a new button into, e.g.,
35 | # hsplit.children, we recreate the layout since we have to
36 | # pay attention to sort order here
37 | event.app.layout = Layout(root_container.create())
38 |
39 | # Find the button in the redrawn layout; then focus on it
40 | buttons = ButtonManager.update_buttons(event.app)
41 | for button in buttons:
42 | if self.name.text == button.content.text()[1][1].strip():
43 | event.app.layout.focus(button)
44 | break
45 |
46 | select_item(event)
47 |
48 | else:
49 | # Add/update server returned an error
50 | ErrorDialog(event, title='Add/edit server error',
51 | text=str(result.get('errors')))
52 |
53 | def cancel_handler():
54 | root_container.floats.pop()
55 | if ButtonManager.prev_button:
56 | event.app.layout.focus(ButtonManager.prev_button)
57 |
58 | def server_to_db():
59 | if self.rowid:
60 | # Updating an existing server
61 | return db.update_one(
62 | rowid=self.rowid,
63 | values={
64 | 'name': self.name.text,
65 | 'url': self.url.text,
66 | 'method': self.method.text
67 | })
68 | else:
69 | # Creating a new server
70 | return db.add_one(
71 | values={
72 | 'name': self.name.text,
73 | 'url': self.url.text,
74 | 'method': self.method.text
75 | })
76 |
77 | # Get data about server currently editing
78 | if create_server:
79 | title = 'New Server'
80 | self.rowid = None
81 | name = ''
82 | url = 'https://'
83 | method = ''
84 | else:
85 | title = 'Edit Server'
86 | self.name = ButtonManager.current_button
87 | result = db.fetch_one(name=self.name)
88 |
89 | self.rowid = result.rowid
90 | name = result.name
91 | url = result.url
92 | method = result.method
93 |
94 | # Dialog configuration
95 | ok_button = Button(text='OK', handler=ok_handler)
96 | cancel_button = Button(text='Cancel', handler=cancel_handler)
97 |
98 | methods = [
99 | 'GET',
100 | 'POST',
101 | 'PUT',
102 | 'HEAD',
103 | 'DELETE',
104 | 'CONNECT',
105 | 'OPTIONS',
106 | 'TRACE',
107 | 'PATCH'
108 | ]
109 | method_completer = WordCompleter(methods,
110 | ignore_case=True)
111 |
112 | local_kb = KeyBindings()
113 |
114 | @local_kb.add('enter')
115 | def cancel_completion(event):
116 | buff = event.app.current_buffer
117 | buff.complete_state = None
118 | event.app.layout.focus_next()
119 |
120 | self.name = TextArea(
121 | multiline=False,
122 | text=name
123 | )
124 | self.name.buffer.document = Document(name, len(name))
125 |
126 | self.url = TextArea(
127 | multiline=False,
128 | text=url
129 | )
130 | self.url.buffer.document = Document(url, len(url))
131 |
132 | self.method = TextArea(
133 | multiline=False,
134 | completer=method_completer,
135 | text=method)
136 | self.method.buffer.document = Document(method, len(method))
137 |
138 | self.dialog = Dialog(
139 | title=title,
140 | body=HSplit([
141 | Label(text='Server name:\n'),
142 | self.name,
143 | Window(height=1, char=' '),
144 | Label(text='Url:\n'),
145 | self.url,
146 | Window(height=1, char=' '),
147 | Label(text='Method:\n'),
148 | self.method,
149 | Window(height=1, char=' ')
150 | ],
151 | key_bindings=local_kb
152 | ),
153 | buttons=[ok_button, cancel_button],
154 | width=D(preferred=80),
155 | with_background=True
156 | )
157 |
158 | self.current_button = event.app.layout.current_window
159 |
160 | root_container.floats.append(Float(self.dialog))
161 | event.app.layout.focus(self.dialog)
162 |
--------------------------------------------------------------------------------
/freud/ui/__init__.py:
--------------------------------------------------------------------------------
1 | from pygments.style import Style
2 | from pygments.token import Keyword, Name, Comment, String, Number
3 |
4 |
5 | class FreudStyle(Style):
6 | default_style = ""
7 | styles = {
8 | Comment: '#776977',
9 | # Bool
10 | Keyword: '#fff',
11 | Name: '#516aec',
12 | Name.Tag: '#516aec',
13 | # JSON Value
14 | String: 'noinherit',
15 | Number: '#ca402b'
16 | }
17 |
--------------------------------------------------------------------------------
/freud/ui/body_container.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout.containers import (
2 | VSplit, HSplit, Window, WindowAlign)
3 | from prompt_toolkit.layout.controls import FormattedTextControl
4 |
5 | from freud.ui.text_buffers import response_box, summary_box, search_toolbar
6 | from freud.ui.server_container import servers
7 | from freud import __version__
8 |
9 |
10 | class TitledBody:
11 |
12 | def create(self, sort_by=None, order=None):
13 |
14 | self.servers = servers.refresh(sort_by=sort_by, order=order)
15 |
16 | servers_output = VSplit([
17 | self.servers,
18 | Window(width=1, char='│', style='class:line'),
19 | response_box.create(),
20 | ])
21 |
22 | body = HSplit([
23 | servers_output,
24 | Window(height=1, char='─', style='class:line'),
25 | summary_box()
26 | ])
27 |
28 | title_bar = [
29 | ('class:title', ' Freud {}'.format(__version__)),
30 | ('class:title',
31 | ' (Press [Ctrl-C] to quit. Press [Ctrl-F] for info.)'),
32 | ]
33 |
34 | self.container = HSplit([
35 | search_toolbar,
36 | Window(height=1,
37 | content=FormattedTextControl(title_bar),
38 | align=WindowAlign.CENTER),
39 | Window(height=1, char='─', style='class:line'),
40 | body,
41 | ])
42 |
43 | return self.container
44 |
45 |
46 | titled_body = TitledBody()
47 |
--------------------------------------------------------------------------------
/freud/ui/dialog.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout.dimension import D
2 | from prompt_toolkit.widgets import Button
3 | from prompt_toolkit.layout.containers import HSplit, Window
4 | from prompt_toolkit.widgets import Dialog
5 | from prompt_toolkit.layout.containers import WindowAlign
6 | from prompt_toolkit.layout import Float
7 | from prompt_toolkit.layout.controls import FormattedTextControl
8 |
9 | from freud.ui.root_container import root_container
10 | from freud.utils import ButtonManager
11 |
12 |
13 | class ErrorDialog:
14 | def __init__(self, event, title='', text=''):
15 |
16 | def ok_handler():
17 | root_container.floats.pop()
18 |
19 | # If there was an original dialog, insert it back into layout
20 | if self.orig_dialog:
21 | root_container.floats.append(self.orig_dialog)
22 | event.app.layout.focus(root_container.float_container)
23 | else:
24 | event.app.layout.focus(ButtonManager.prev_button)
25 |
26 | ok_button = Button(text='OK', handler=ok_handler)
27 |
28 | dialog = Dialog(
29 | Window(
30 | wrap_lines=True,
31 | content=FormattedTextControl(text=text),
32 | always_hide_cursor=True
33 | ),
34 | title=title,
35 | width=D(preferred=80),
36 | buttons=[ok_button],
37 | with_background=True
38 | )
39 |
40 | try:
41 | # If a dialog was already up, save it
42 | self.orig_dialog = root_container.floats.pop()
43 | except IndexError:
44 | self.orig_dialog = None
45 |
46 | root_container.floats.append(Float(content=dialog))
47 | event.app.layout.focus(ok_button)
48 |
49 |
50 | class LoadingDialog:
51 |
52 | """ Displays a loading message when waiting for a server response """
53 |
54 | def __init__(
55 | self, event=None, title='', text=''):
56 |
57 | content = Window(
58 | height=3,
59 | align=WindowAlign.CENTER,
60 | content=FormattedTextControl(
61 | text=text,
62 | show_cursor=False,
63 | modal=True
64 | )
65 | )
66 |
67 | body = HSplit([
68 | Window(height=1, char=' '),
69 | content,
70 | Window(height=1, char=' '),
71 | ], padding=1)
72 |
73 | dialog = Dialog(
74 | body,
75 | title=title,
76 | width=D(preferred=80),
77 | with_background=True
78 | )
79 |
80 | loading_float = Float(content=dialog)
81 |
82 | root_container.floats.append(loading_float)
83 |
84 | self.focus = event.app.layout.focus
85 | self.focus(content)
86 |
87 | def close_dialog(self):
88 | root_container.floats.pop()
89 | self.focus(ButtonManager.prev_button)
90 |
--------------------------------------------------------------------------------
/freud/ui/keys.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout.dimension import D
2 | from prompt_toolkit import HTML
3 | from prompt_toolkit.widgets import Button, Dialog, Label
4 | from prompt_toolkit.layout.containers import (
5 | VSplit, HSplit, Window, HorizontalAlign)
6 | from prompt_toolkit.layout import Float
7 |
8 | from freud.utils import ButtonManager
9 | from freud.ui.root_container import root_container
10 | from freud.ui.server_container import servers
11 | from freud import KEYS
12 |
13 |
14 | class KeyQuickRef:
15 |
16 | """ Displays a quick reference for key bindings """
17 |
18 | def __init__(self, event):
19 |
20 | def ok_handler():
21 |
22 | root_container.floats.pop()
23 | if ButtonManager.prev_button:
24 | event.app.layout.focus(ButtonManager.prev_button)
25 | else:
26 | event.app.layout.focus(servers.content)
27 |
28 | ok_button = Button(text='OK', handler=ok_handler)
29 |
30 | # Create dialog with title
31 | self.input = HSplit([
32 | Label(text=HTML(
33 | 'Default Vi key bindings.')
34 | ),
35 | Window(height=1, char=' ')
36 | ])
37 |
38 | # Add navigation keys to dialog
39 | self.input.children.append(VSplit([
40 | Label(text=HTML('Basic navigation\n'))])
41 | )
42 | for key in navigation_keys:
43 | new_key = KeyFactory(
44 | key=key, value=navigation_keys[key])
45 | self.input.children.append(new_key.vsplit)
46 |
47 | spacer = Window(height=1, char='─')
48 | blank_spacer = Window(height=1, char=' ')
49 |
50 | self.input.children.append(blank_spacer)
51 | self.input.children.append(spacer)
52 |
53 | # Add server keys to dialog
54 | self.input.children.append(VSplit([
55 | Label(text=HTML('Server keys\n'))])
56 | )
57 | for key in server_keys:
58 | new_key = KeyFactory(
59 | key=key, value=server_keys[key])
60 | self.input.children.append(new_key.vsplit)
61 |
62 | self.input.children.append(blank_spacer)
63 | self.input.children.append(spacer)
64 |
65 | # Add response keys to dialog
66 | self.input.children.append(VSplit([
67 | Label(
68 | text=HTML(
69 | 'Text areas (response, headers, summary)\n'))])
70 | )
71 | for key in response_body_keys:
72 | new_key = KeyFactory(
73 | key=key, value=response_body_keys[key])
74 | self.input.children.append(new_key.vsplit)
75 | self.input.children.append(blank_spacer)
76 | self.input.children.append(blank_spacer)
77 |
78 | self.dialog = Dialog(
79 | title='Key Quick Reference',
80 | body=self.input,
81 | buttons=[ok_button],
82 | width=D(preferred=80),
83 | with_background=True,
84 | modal=True)
85 |
86 | root_container.floats.append(Float(self.dialog))
87 | event.app.layout.focus(self.dialog)
88 |
89 |
90 | class KeyFactory:
91 |
92 | def __init__(self, key=None, value=None):
93 |
94 | self.key = Label(
95 | text=key,
96 | width=20
97 | )
98 | self.value = Label(
99 | text=value,
100 | )
101 | self.vsplit = VSplit([
102 | self.key,
103 | Window(width=1, char=' '),
104 | self.value
105 | ], align=HorizontalAlign.LEFT)
106 |
107 |
108 | # leader = KEYS['leader']
109 |
110 | navigation_keys = {
111 | 'tab': 'Switch windows',
112 | 'j/down': 'Next server',
113 | 'k/up': 'Previous server',
114 | 'g': 'First server',
115 | 'G': 'Last server',
116 | }
117 |
118 | server_keys = {
119 | KEYS['send_request']: 'Send request',
120 | KEYS['new_server']: 'New server',
121 | KEYS['edit_server']: 'Edit server',
122 | KEYS['edit_headers']: 'Edit headers',
123 | KEYS['edit_authentication']: 'Edit auth',
124 | KEYS['edit_body']: 'Edit body',
125 | KEYS['delete_server']: 'Delete server',
126 | }
127 |
128 | response_body_keys = {
129 | '/': 'Forward search',
130 | '?': 'Backward search',
131 | 'n/N': 'Next/Previous search term',
132 | 'Vi Navigation': 'e.g., gg, G, ctrl+u, ctrl+d, etc.',
133 | }
134 |
--------------------------------------------------------------------------------
/freud/ui/root_container.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout.containers import HSplit
2 | from prompt_toolkit.layout import FloatContainer
3 | from prompt_toolkit.layout import Float
4 | from prompt_toolkit.layout.menus import CompletionsMenu
5 |
6 | from freud.ui.body_container import titled_body
7 | from freud.utils import SortOrder
8 |
9 |
10 | class RootContainer:
11 |
12 | def create(self):
13 |
14 | sort_by = SortOrder.sort_by
15 | order = SortOrder.order
16 |
17 | self.body = titled_body.create(sort_by=sort_by, order=order)
18 |
19 | self.container = HSplit([self.body])
20 |
21 | completions = Float(xcursor=True,
22 | ycursor=True,
23 | content=CompletionsMenu(max_height=16, scroll_offset=1))
24 |
25 | self.float_container = FloatContainer(
26 | content=self.container, floats=[completions])
27 |
28 | self.floats = self.float_container.floats
29 |
30 | return self.float_container
31 |
32 |
33 | root_container = RootContainer()
34 |
--------------------------------------------------------------------------------
/freud/ui/server_container.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout import HSplit
2 | from prompt_toolkit.widgets import Box
3 |
4 | from freud.utils import CustomButton
5 | from freud.utils import ButtonManager
6 | from freud.model import db
7 | from freud import SERVER_WIDTH
8 |
9 |
10 | def update_servers(sort_by=None, order=None):
11 |
12 | rows = db.fetch_all(sort_by=sort_by, order=order)
13 | values = []
14 | for row in rows:
15 | values.append(row[0])
16 |
17 | return values
18 |
19 |
20 | class ServerContainer:
21 |
22 | def refresh(self, sort_by=None, order=None):
23 |
24 | names = update_servers(sort_by=sort_by, order=order)
25 |
26 | self.server_list = []
27 | for name in names:
28 | button_obj = ButtonManager(name)
29 | button = CustomButton(name, handler=button_obj.click_handler)
30 | self.server_list.append(button)
31 |
32 | # If there are no servers
33 | if not self.server_list:
34 | from prompt_toolkit.layout.containers import Window
35 | from prompt_toolkit.layout.controls import FormattedTextControl
36 |
37 | window = Window(
38 | FormattedTextControl(
39 | 'No Servers',
40 | focusable=True,
41 | show_cursor=False
42 | )
43 | )
44 |
45 | self.server_list.append(window)
46 |
47 | self.hsplit = HSplit(self.server_list)
48 |
49 | boxes = Box(
50 | width=SERVER_WIDTH,
51 | padding_top=0,
52 | padding_left=1,
53 | body=self.hsplit
54 | )
55 |
56 | from freud.key_bindings import server_kb
57 |
58 | self.content = HSplit([
59 | boxes
60 | ], key_bindings=server_kb)
61 |
62 | return self.content
63 |
64 |
65 | servers = ServerContainer()
66 |
--------------------------------------------------------------------------------
/freud/ui/sort.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.layout.dimension import D
2 | from prompt_toolkit.layout import Float
3 | from prompt_toolkit.layout import Layout
4 | from prompt_toolkit.widgets import Button, Dialog, RadioList
5 | from prompt_toolkit.key_binding import KeyBindings
6 |
7 | from freud.utils import ButtonManager, SortOrder
8 | from freud.ui.root_container import root_container
9 | from freud.ui.server_container import servers
10 |
11 |
12 | class SortDialog:
13 | def __init__(self, event):
14 | kb = KeyBindings()
15 |
16 | def ok_handler():
17 |
18 | sort_by = self.radio_list.current_value['sort_by']
19 | order = self.radio_list.current_value['order']
20 |
21 | SortOrder.sort_by = sort_by
22 | SortOrder.order = order
23 |
24 | root_container.floats.pop()
25 |
26 | event.app.layout = Layout(root_container.create(),
27 | focused_element=servers.content
28 | )
29 | ButtonManager.prev_button = event.app.layout.current_window
30 |
31 | def cancel_handler():
32 | root_container.floats.pop()
33 | root_container.float_container.key_bindings = None
34 | event.app.layout.focus(ButtonManager.prev_button)
35 |
36 | ok_button = Button(text='OK', handler=ok_handler)
37 | cancel_button = Button(text='Cancel', handler=cancel_handler)
38 |
39 | self.radio_list = RadioList(values=[
40 | ({'sort_by': 'name', 'order': 'asc'}, 'Name'),
41 | ({'sort_by': 'name', 'order': 'desc'}, 'Name - Desc'),
42 | ({'sort_by': 'timestamp', 'order': 'asc'}, 'Time Added'),
43 | ({'sort_by': 'timestamp', 'order': 'desc'}, 'Time Added - Desc')
44 | ])
45 |
46 | kb = self.radio_list.control.key_bindings
47 |
48 | @kb.add('j')
49 | def down(event):
50 | self.radio_list._selected_index = min(
51 | len(self.radio_list.values) - 1,
52 | self.radio_list._selected_index + 1
53 | )
54 |
55 | @kb.add('k')
56 | def up(event):
57 | self.radio_list._selected_index = max(
58 | 0, self.radio_list._selected_index - 1)
59 |
60 | @kb.add('g', 'g')
61 | def top(event):
62 | self.radio_list._selected_index = 0
63 |
64 | @kb.add('G')
65 | def bottom(event):
66 | self.radio_list._selected_index = len(self.radio_list.values) - 1
67 |
68 | self.dialog = Dialog(
69 | title='Sort',
70 | body=self.radio_list,
71 | buttons=[ok_button, cancel_button],
72 | width=D(preferred=80),
73 | with_background=True,
74 | modal=True)
75 |
76 | root_container.float_container.key_bindings = kb
77 |
78 | root_container.floats.append(Float(self.dialog))
79 | event.app.layout.focus(self.dialog)
80 |
--------------------------------------------------------------------------------
/freud/ui/style.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.styles import Style
2 | from prompt_toolkit.styles.pygments import style_from_pygments_cls
3 | from pygments.styles import get_style_by_name
4 |
5 | from freud import STYLE
6 |
7 | theme = STYLE['theme']
8 | line_fg = STYLE.get('separator_line_fg', '')
9 | line_bg = STYLE.get('separator_line_bg', '')
10 |
11 |
12 | custom_style = [
13 | ('line', 'bg:{} fg:{}'.format(line_bg, line_fg)),
14 | ('button.selected', 'bg:#cccccc fg:#880000'),
15 | ]
16 |
17 | style = style_from_pygments_cls(get_style_by_name(theme))
18 |
19 | style._style_rules.extend(custom_style)
20 |
--------------------------------------------------------------------------------
/freud/ui/text_buffers.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.buffer import Buffer
2 | from prompt_toolkit.layout.controls import BufferControl
3 | from prompt_toolkit.lexers import PygmentsLexer
4 | from prompt_toolkit.layout.margins import ScrollbarMargin, NumberedMargin
5 | from prompt_toolkit.layout.containers import Window
6 | from pygments.lexers import JsonLexer
7 | from prompt_toolkit.layout import HSplit
8 |
9 | from prompt_toolkit.widgets import SearchToolbar
10 |
11 | from pygments.lexer import RegexLexer, bygroups
12 | from pygments.token import Text, Keyword, String, Name
13 |
14 | from freud import HEADER_HEIGHT, SUMMARY_HEIGHT
15 |
16 | response_buffer = Buffer(
17 | read_only=True, enable_history_search=True, name='response_buffer')
18 | header_buffer = Buffer(read_only=True, name='header_buffer')
19 | summary_buffer = Buffer(read_only=True, name='summary_buffer')
20 |
21 | search_toolbar = SearchToolbar()
22 |
23 |
24 | class HeaderLexer(RegexLexer):
25 | tokens = {
26 | 'root': [
27 | (r'(\[)(\')(\d+.*?)(\')(\] )(\| )(\d+.*?\n)',
28 | bygroups(Keyword, None, Name, None, Keyword, None, Text)),
29 | (r'(\S+:\s)(.*?$)',
30 | bygroups(Name, Text))
31 | ]
32 | }
33 |
34 |
35 | class ResponseBox:
36 | def create(self):
37 | from freud.key_bindings import response_kb, header_kb
38 |
39 | right_margins = [ScrollbarMargin(display_arrows=True)]
40 | left_margins = [NumberedMargin()]
41 |
42 | self.buffer_control = BufferControl(
43 | lexer=PygmentsLexer(JsonLexer),
44 | search_buffer_control=search_toolbar.control,
45 | buffer=response_buffer)
46 |
47 | header_window = Window(
48 | wrap_lines=True,
49 | right_margins=right_margins,
50 | left_margins=left_margins,
51 | height=HEADER_HEIGHT,
52 | content=BufferControl(
53 | key_bindings=header_kb,
54 | search_buffer_control=search_toolbar.control,
55 | lexer=PygmentsLexer(HeaderLexer), buffer=header_buffer)
56 | )
57 |
58 | body_window = Window(
59 | left_margins=left_margins,
60 | right_margins=right_margins,
61 | wrap_lines=True,
62 | content=self.buffer_control
63 | )
64 |
65 | return HSplit([
66 | header_window,
67 | Window(height=1, char='─', style='class:line'),
68 | body_window,
69 | ], key_bindings=response_kb)
70 |
71 |
72 | response_box = ResponseBox()
73 |
74 |
75 | def summary_box():
76 |
77 | from freud.key_bindings import summary_kb
78 |
79 | return Window(
80 | content=BufferControl(
81 | key_bindings=summary_kb,
82 | lexer=PygmentsLexer(JsonLexer), buffer=summary_buffer
83 | ), height=SUMMARY_HEIGHT
84 | )
85 |
--------------------------------------------------------------------------------
/freud/utils.py:
--------------------------------------------------------------------------------
1 | import six
2 | import json
3 |
4 | from prompt_toolkit.application import get_app
5 | from prompt_toolkit.filters import to_filter
6 | from prompt_toolkit.mouse_events import MouseEventType
7 | from prompt_toolkit.key_binding.key_bindings import KeyBindings
8 | from prompt_toolkit.layout.containers import Window
9 | from prompt_toolkit.layout.controls import FormattedTextControl
10 |
11 | from freud.model import db
12 | from freud.ui.text_buffers import summary_buffer
13 |
14 |
15 | class ButtonManager:
16 | """
17 | Button logic flows through here. Button Manager saves the current button,
18 | previous button, list of buttons, and provides a click handler.
19 | """
20 |
21 | current_button = None
22 | prev_button = None
23 | buttons = None
24 |
25 | def __init__(self, name):
26 | self.name = name
27 |
28 | def click_handler(self):
29 |
30 | result = db.fetch_one(name=self.name)
31 |
32 | output = {
33 | 'name': result.name,
34 | 'method': result.method,
35 | 'url': result.url
36 | }
37 |
38 | if result.headers:
39 | output.update({
40 | 'headers': json.loads(result.headers)
41 | })
42 | if result.body:
43 | try:
44 | body = json.loads(result.body)
45 | except json.decoder.JSONDecodeError:
46 | body = str(result.body).splitlines()
47 |
48 | output.update({
49 | 'body': body
50 | })
51 | if result.auth:
52 | auth = json.loads(result.auth)
53 | output.update({
54 | 'auth': {
55 | 'type': auth['type'],
56 | 'user': auth['user']
57 | }
58 | })
59 |
60 | summary_buffer.read_only = to_filter(False)
61 | summary_buffer.text = json.dumps(output, indent=2)
62 | summary_buffer.read_only = to_filter(True)
63 |
64 | type(self).current_button = self.name
65 |
66 | app = get_app()
67 | type(self).prev_button = app.layout.current_window
68 |
69 | @classmethod
70 | def update_buttons(cls, app):
71 |
72 | windows = [w for w in app.layout.find_all_windows()]
73 | cls.buttons = []
74 | for window in windows:
75 | if 'CustomButton' in str(window):
76 | cls.buttons.append(window)
77 |
78 | if cls.prev_button is None and len(cls.buttons) > 0:
79 | cls.prev_button = cls.buttons[0]
80 |
81 | return cls.buttons
82 |
83 |
84 | class SortOrder:
85 | """ Saves the sort order for reference by application """
86 |
87 | sort_by = None
88 | order = None
89 |
90 |
91 | class CustomButton:
92 | """
93 | Taken from Python Prompt Toolkit's Button class for customization
94 |
95 | Clickable button.
96 | :param text: The caption for the button.
97 | :param handler: `None` or callable. Called when the button is clicked.
98 | :param width: Width of the button.
99 | """
100 |
101 | def __init__(self, text, handler=None):
102 | assert isinstance(text, six.text_type)
103 | assert handler is None or callable(handler)
104 |
105 | self.text = text
106 | self.handler = handler
107 | self.control = FormattedTextControl(
108 | self._get_text_fragments,
109 | key_bindings=self._get_key_bindings(),
110 | show_cursor=False,
111 | focusable=True)
112 |
113 | def get_style():
114 | if get_app().layout.has_focus(self):
115 | return 'class:button.focused'
116 |
117 | return 'class:button'
118 |
119 | self.window = Window(
120 | self.control,
121 | height=1,
122 | style=get_style,
123 | dont_extend_width=True,
124 | dont_extend_height=True)
125 |
126 | def _get_text_fragments(self):
127 | text = self.text
128 |
129 | def handler(mouse_event):
130 | if mouse_event.event_type == MouseEventType.MOUSE_UP:
131 | self.handler()
132 |
133 | return [
134 | ('class:button.arrow', '', handler),
135 | ('class:button.text', text, handler),
136 | ('class:button.arrow', '', handler),
137 | ]
138 |
139 | def _get_key_bindings(self):
140 | kb = KeyBindings()
141 |
142 | @kb.add(' ')
143 | @kb.add('enter')
144 | def _(event):
145 | if self.handler is not None:
146 | self.handler()
147 |
148 | return kb
149 |
150 | def __pt_container__(self):
151 | return self.window
152 |
153 |
154 | def on_startup(app):
155 | """ Run from __main__ after render """
156 |
157 | ButtonManager.update_buttons(app)
158 | if not summary_buffer.text:
159 | # When starting app, select first server if none selected
160 | select_item(app)
161 |
162 |
163 | class SingleClick:
164 | """ Provides mouse click key """
165 |
166 | def __init__(self):
167 | self.event_type = MouseEventType.MOUSE_UP
168 |
169 |
170 | def select_item(event):
171 | """ Simulate mouse click """
172 |
173 | # Don't try to select button if there are none
174 | if len(ButtonManager.buttons) > 0:
175 |
176 | try:
177 |
178 | event.app.layout.current_window.content.text()[1][2](SingleClick())
179 |
180 | # If app is passed in, rather than event
181 | except AttributeError:
182 |
183 | event.layout.current_window.content.text()[1][2](SingleClick())
184 |
--------------------------------------------------------------------------------
/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stloma/freud/ebc16ecf7bd39ad365108e1cce4c9e8a0ddc69cd/img/demo.gif
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | with open('README.md', encoding='utf-8') as f:
4 | long_description = f.read()
5 |
6 | setup(
7 | name='freud',
8 | version='0.1.3.dev0',
9 | author='Stephen Martin',
10 | author_email='lockwood@opperline.com',
11 | description='TUI REST client to analyze API endpoints',
12 | long_description=long_description,
13 | long_description_content_type='text/markdown',
14 | url='https://github.com/stloma/freud',
15 | classifiers=[
16 | 'Development Status :: 3 - Alpha',
17 | 'Intended Audience :: Developers',
18 | 'Topic :: Software Development :: Build Tools',
19 | 'License :: OSI Approved :: MIT License',
20 | 'Programming Language :: Python :: 3.5',
21 | 'Programming Language :: Python :: 3.6',
22 | ],
23 | keywords='cli tui http rest request api',
24 | packages=find_packages(),
25 | install_requires=[
26 | 'requests==2.21.0',
27 | 'prompt_toolkit==2.0.7',
28 | 'pygments==2.2.0'
29 | ],
30 | extras_require={
31 | 'dev': [
32 | 'flake8==3.6.0',
33 | 'pytest==4.1.0',
34 | 'pytest_asyncio==0.10.0',
35 | 'pytest_httpbin==0.3.0',
36 | 'tox==3.6.1'
37 | ]
38 | },
39 | entry_points={
40 | 'console_scripts': [
41 | 'freud=freud.__main__:main',
42 | ],
43 | 'pygments.styles': [
44 | 'freud=freud.ui:FreudStyle'
45 | ]
46 | },
47 | project_urls={
48 | 'Bug Reports': 'https://github.com/stloma/freud/issues',
49 | 'Source': 'https://github.com/stloma/freud',
50 | },
51 | )
52 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stloma/freud/ebc16ecf7bd39ad365108e1cce4c9e8a0ddc69cd/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import json
3 | import os
4 | import sys
5 |
6 |
7 | def pytest_configure():
8 | """ Freud checks for this upon startup. If true, creates a new test.db """
9 |
10 | sys._called_from_test = True
11 |
12 |
13 | def pytest_unconfigure():
14 | from freud import DB_FILE
15 | from freud.model import basedir
16 |
17 | del sys._called_from_test
18 | os.remove(os.path.join(basedir, DB_FILE))
19 |
20 |
21 | @pytest.fixture(scope='class')
22 | def db_conn():
23 |
24 | from freud.model import Db
25 | db = Db()
26 |
27 | yield db
28 |
29 |
30 | @pytest.fixture(scope='function')
31 | def db(db_conn):
32 |
33 | db_conn.delete_all()
34 |
35 | return db_conn
36 |
37 |
38 | @pytest.fixture(scope='function')
39 | def db_dummy_data(db):
40 | db.add_one(
41 | {'name': 'alice', 'url': 'alice.com', 'method': 'get'})
42 |
43 | db.add_one(
44 | {'name': 'bob', 'url': 'bob.com', 'method': 'get'})
45 |
46 | return
47 |
48 |
49 | @pytest.fixture(scope='class')
50 | def db_request_data(httpbin, db_conn):
51 |
52 | # Get request
53 | #
54 | db_conn.add_one({
55 | 'name': 'httpbin-get',
56 | 'url': '{}/get'.format(httpbin.url),
57 | 'method': 'GET'
58 | })
59 |
60 | # Delete request
61 | #
62 | db_conn.add_one({
63 | 'name': 'httpbin-delete',
64 | 'url': '{}/delete'.format(httpbin.url),
65 | 'method': 'DELETE',
66 | })
67 |
68 | # Patch request
69 | #
70 | db_conn.add_one({
71 | 'name': 'httpbin-patch',
72 | 'url': '{}/patch'.format(httpbin.url),
73 | 'method': 'PATCH',
74 | })
75 |
76 | # Put request
77 | #
78 | db_conn.add_one({
79 | 'name': 'httpbin-put',
80 | 'url': '{}/put'.format(httpbin.url),
81 | 'method': 'PUT',
82 | })
83 |
84 | # Post request
85 | #
86 | db_conn.add_one({
87 | 'name': 'httpbin-post',
88 | 'url': '{}/post'.format(httpbin.url),
89 | 'method': 'POST',
90 | 'body': json.dumps({'name': 'alice'}),
91 | 'headers': json.dumps({'Content-Type': 'application/json'})
92 | })
93 |
94 | # Post form
95 | #
96 | db_conn.add_one({
97 | 'name': 'httpbin-post-form',
98 | 'url': '{}/post'.format(httpbin.url),
99 | 'method': 'POST',
100 | 'headers': json.dumps(
101 | {'content-type': 'application/x-www-form-urlencoded'}
102 | ),
103 | 'body': 'name=alice&email=alice@email.com'
104 | })
105 |
106 | # Basic auth
107 | #
108 | db_conn.add_one({
109 | 'name': 'httpbin-basic-auth-200',
110 | 'url': '{}/basic-auth/alice/password'.format(httpbin.url),
111 | 'method': 'GET',
112 | 'auth': json.dumps({
113 | 'user': 'alice',
114 | 'password': 'password',
115 | 'type': 'basic'
116 | })
117 | })
118 | db_conn.add_one({
119 | 'name': 'httpbin-basic-auth-401',
120 | 'url': '{}/basic-auth/alice/password'.format(httpbin.url),
121 | 'method': 'GET',
122 | 'auth': json.dumps({
123 | 'user': 'eve',
124 | 'password': 'bad_password',
125 | 'type': 'basic'
126 | })
127 | })
128 | db_conn.add_one({
129 | 'name': 'httpbin-digest-auth-200',
130 | 'url': '{}/digest-auth/auth/alice/password'.format(httpbin.url),
131 | 'method': 'GET',
132 | 'auth': json.dumps({
133 | 'user': 'alice',
134 | 'password': 'password',
135 | 'type': 'digest'
136 | })
137 | })
138 | db_conn.add_one({
139 | 'name': 'httpbin-digest-auth-401',
140 | 'url': '{}/digest-auth/auth/alice/password'.format(httpbin.url),
141 | 'method': 'GET',
142 | 'auth': json.dumps({
143 | 'user': 'eve',
144 | 'password': 'bad_password',
145 | 'type': 'digest'
146 | })
147 | })
148 |
149 | return
150 |
--------------------------------------------------------------------------------
/tests/test_database.py:
--------------------------------------------------------------------------------
1 | class TestCRUD:
2 | def test_add_one(self, db):
3 | result = db.add_one(
4 | {'name': 'alice', 'url': 'alice.com', 'method': 'get'})
5 |
6 | errors = result.get('errors')
7 | success = result.get('success')
8 |
9 | assert errors is None
10 | assert success is True
11 |
12 | def test_fetch_one(self, db, db_dummy_data):
13 | result = db.fetch_one(name='alice')
14 |
15 | assert result.name == 'alice'
16 | assert result.url == 'alice.com'
17 | assert result.method == 'get'
18 |
19 | def test_update_one(self, db, db_dummy_data):
20 | result = db.fetch_one(name='alice')
21 |
22 | db.update_one(rowid=result.rowid, values={
23 | 'name': 'eve', 'method': 'post', 'url': 'example.com'})
24 |
25 | result = db.fetch_one(name='eve')
26 |
27 | assert result.name == 'eve'
28 | assert result.method == 'post'
29 |
30 | def test_fetch_all(self, db, db_dummy_data):
31 | rows = db.fetch_all()
32 |
33 | assert len(rows) == 2
34 |
35 | def test_delete_one(self, db, db_dummy_data):
36 | before_delete = db.fetch_all()
37 |
38 | db.delete_one(name='alice')
39 |
40 | after_delete = db.fetch_all()
41 |
42 | assert len(before_delete) == len(after_delete) + 1
43 |
44 |
45 | class TestErrors:
46 |
47 | def test_add_one_missing_column(self, db):
48 | result = db.add_one({})
49 |
50 | errors = result.get('errors')
51 |
52 | assert errors == 'missing column error: name'
53 |
54 | result = db.add_one(
55 | {'name': 'alice'})
56 |
57 | errors = result.get('errors')
58 |
59 | assert errors == 'missing column error: url'
60 |
61 | result = db.add_one(
62 | {'name': 'alice', 'url': 'alice.com'})
63 |
64 | errors = result.get('errors')
65 |
66 | assert errors == 'missing column error: method'
67 |
68 | result = db.add_one(
69 | {'name': 'alice', 'url': 'alice.com', 'method': 'get'})
70 |
71 | errors = result.get('errors')
72 |
73 | assert errors is None
74 |
75 | def test_constraint_violation(self, db, db_dummy_data):
76 | result = db.add_one(
77 | {'name': 'alice', 'url': 'alice.com', 'method': 'get'})
78 |
79 | errors = result.get('errors')
80 |
81 | assert errors == '{}'.format(
82 | 'sqlite error: UNIQUE constraint failed: requests.name')
83 |
84 |
85 | class TestSort:
86 |
87 | def test_sort(self, db, db_dummy_data):
88 |
89 | name_asc = db.fetch_all(sort_by='name', order='asc')
90 | assert name_asc[0].name == 'alice'
91 | assert name_asc[1].name == 'bob'
92 |
93 | name_desc = db.fetch_all(sort_by='name', order='desc')
94 | assert name_desc[0].name == 'bob'
95 | assert name_desc[1].name == 'alice'
96 |
97 | time_asc = db.fetch_all(sort_by='timestamp', order='asc')
98 | assert time_asc[0].name == 'alice'
99 | assert time_asc[1].name == 'bob'
100 |
101 | time_desc = db.fetch_all(sort_by='timestamp', order='desc')
102 | assert time_desc[0].name == 'bob'
103 | assert time_desc[1].name == 'alice'
104 |
105 |
--------------------------------------------------------------------------------
/tests/test_layout.py:
--------------------------------------------------------------------------------
1 | import json
2 | from prompt_toolkit.layout.layout import Layout
3 | from .utils import SingleClick
4 | from freud.utils import ButtonManager
5 | from freud.ui.server_container import servers
6 | from freud.ui.root_container import root_container
7 | from freud.key_bindings import server_kb
8 |
9 |
10 | class TestChangingLayout:
11 |
12 | @staticmethod
13 | def _get_windows(root_container):
14 | root_container = root_container.create()
15 | layout = Layout(root_container)
16 | all_windows = layout.find_all_windows()
17 | windows = []
18 | for window in all_windows:
19 | windows.append(window)
20 |
21 | return windows
22 |
23 | @staticmethod
24 | def _get_all_windows(layout):
25 | return [w for w in layout.find_all_windows()]
26 |
27 | def test_check_layout(self, db_dummy_data):
28 |
29 | windows = self._get_windows(root_container)
30 |
31 | search_buffer = windows[0]
32 | title_window = windows[1]
33 | dummy_one = windows[4]
34 | button_one = windows[5]
35 | button_two = windows[6]
36 | dummy_two = windows[7]
37 | header_buffer = windows[10]
38 | response_buffer = windows[12]
39 | summary_buffer = windows[14]
40 |
41 | assert 'SearchBuffer' in str(search_buffer)
42 | assert 'Freud' in str(title_window)
43 | assert 'CustomButton' not in str(dummy_one)
44 | assert 'CustomButton' in str(button_one)
45 | assert 'CustomButton' in str(button_two)
46 | assert 'CustomButton' not in str(dummy_two)
47 | assert 'header_buffer' in str(header_buffer)
48 | assert 'response_buffer' in str(response_buffer)
49 | assert 'summary_buffer' in str(summary_buffer)
50 |
51 | def test_update_response_box(self, db_dummy_data):
52 |
53 | # Create layout
54 | layout = Layout(root_container.create(),
55 | focused_element=servers.content)
56 |
57 | summary_buffer = layout.get_buffer_by_name('summary_buffer')
58 |
59 | # Simulate mouse click on first server
60 | layout.current_window.content.text()[1][2](SingleClick())
61 | summary_buffer_before = json.loads(summary_buffer.text)
62 |
63 | # Get buttons for focus
64 | app = App(layout)
65 | ButtonManager.update_buttons(app)
66 |
67 | # Focus and simulate click on next server
68 | layout.focus(ButtonManager.buttons[1])
69 | layout.current_window.content.text()[1][2](SingleClick())
70 | summary_buffer_after = json.loads(summary_buffer.text)
71 |
72 | assert summary_buffer_before['name'] == 'alice'
73 | assert summary_buffer_after['name'] == 'bob'
74 |
75 | def test_add_server(self, db, db_dummy_data):
76 | db.add_one({'name': 'john', 'url': 'john.com', 'method': 'get'})
77 |
78 | windows = self._get_windows(root_container)
79 |
80 | dummy_one = windows[4]
81 | button_one = windows[5]
82 | button_two = windows[6]
83 | button_three = windows[7]
84 | dummy_two = windows[8]
85 |
86 | assert 'CustomButton' not in str(dummy_one)
87 | assert 'CustomButton' in str(button_one)
88 | assert 'CustomButton' in str(button_two)
89 | assert 'CustomButton' in str(button_three)
90 | assert 'CustomButton' not in str(dummy_two)
91 |
92 | def test_delete_server(
93 | self, db, db_dummy_data):
94 |
95 | # Get handler for delete server key binding
96 | for key in server_kb.bindings:
97 | if 'rm_server' in str(key):
98 | delete_handler = key.handler
99 |
100 | # Create layout
101 | root = root_container.create()
102 | layout = Layout(root,
103 | focused_element=servers.content)
104 | windows = self._get_all_windows(layout)
105 |
106 | event = Event(layout)
107 |
108 | # Simulate mouse click on first server
109 | layout.current_window.content.text()[1][2](SingleClick())
110 |
111 | # Simulate pressing key binding for deleting a server
112 | delete_handler(event)
113 |
114 | # When a server is deleted in the app we get a delete confirmation
115 | # dialog, which is stored in root.floats. Check for that
116 | # here.
117 | windows = self._get_all_windows(layout)
118 | floats = root.floats
119 |
120 | assert len(floats) == 2
121 |
122 | # Simulate pressing the OK button in the delete confirmation dialog
123 | windows[37].content.text()[2][2](SingleClick())
124 |
125 | assert len(floats) == 1
126 |
127 | windows = self._get_all_windows(layout)
128 |
129 | dummy_one = str(windows[4])
130 | button_one = str(windows[5])
131 | dummy_two = str(windows[6])
132 |
133 | assert 'Button' not in dummy_one
134 | assert 'Button' in button_one
135 | assert 'Button' not in dummy_two
136 |
137 | def test_sort_dialog(self, db_dummy_data):
138 |
139 | for key in server_kb.bindings:
140 | if 'sort' in str(key):
141 | sort_handler = key.handler
142 |
143 | # Create layout
144 | root = root_container.create()
145 | layout = Layout(root,
146 | focused_element=servers.content)
147 | windows = self._get_all_windows(layout)
148 |
149 | assert len(root_container.floats) == 1
150 |
151 | event = Event(layout)
152 | sort_handler(event)
153 |
154 | assert len(root_container.floats) == 2
155 |
156 | windows = self._get_all_windows(layout)
157 | windows[36].content.text()[2][2](SingleClick())
158 |
159 | assert len(root_container.floats) == 1
160 |
161 |
162 | class Event:
163 | def __init__(self, layout):
164 | self.app = App(layout)
165 | self.event = self.app
166 |
167 |
168 | class App:
169 | def __init__(self, layout):
170 | self.layout = layout
171 |
--------------------------------------------------------------------------------
/tests/test_requests.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import json
3 |
4 | from freud.api.request import request_handler
5 | from freud.ui.text_buffers import response_box
6 |
7 |
8 | class TestResponses:
9 |
10 | @pytest.mark.asyncio
11 | async def test_get_request(self, db_request_data):
12 | # Need to create response_box here, so response_box.lexer is available
13 | response_box.create()
14 |
15 | response = await request_handler('httpbin-get')
16 | headers, _ = response.get('response')
17 |
18 | assert headers.startswith("['200 Ok']")
19 |
20 | @pytest.mark.asyncio
21 | async def test_delete_request(self, db_request_data):
22 |
23 | response = await request_handler('httpbin-delete')
24 | headers, _ = response.get('response')
25 |
26 | assert headers.startswith("['200 Ok']")
27 |
28 | @pytest.mark.asyncio
29 | async def test_patch_request(self, db_request_data):
30 |
31 | response = await request_handler('httpbin-patch')
32 | headers, _ = response.get('response')
33 |
34 | assert headers.startswith("['200 Ok']")
35 |
36 | @pytest.mark.asyncio
37 | async def test_post_request(self, db_request_data):
38 |
39 | response = await request_handler('httpbin-post')
40 | _, output = response.get('response')
41 |
42 | r = json.loads(output)
43 | post_data = r['data']
44 |
45 | assert post_data == '{"name": "alice"}'
46 |
47 | @pytest.mark.asyncio
48 | async def test_put_request(self, db_request_data):
49 |
50 | response = await request_handler('httpbin-put')
51 | headers, _ = response.get('response')
52 |
53 | assert headers.startswith("['200 Ok']")
54 |
55 | @pytest.mark.asyncio
56 | async def test_post_form(self, db_request_data):
57 |
58 | response = await request_handler('httpbin-post-form')
59 | _, output = response.get('response')
60 |
61 | r = json.loads(output)
62 | form_data = r['form']
63 |
64 | assert form_data['email'] == 'alice@email.com'
65 | assert form_data['name'] == 'alice'
66 |
67 | @pytest.mark.asyncio
68 | async def test_basic_auth(self, db_request_data):
69 |
70 | response = await request_handler('httpbin-basic-auth-200')
71 | headers, _ = response.get('response')
72 |
73 | assert headers.startswith("['200 Ok']")
74 |
75 | response = await request_handler('httpbin-basic-auth-401')
76 | headers, _ = response.get('response')
77 |
78 | assert headers.startswith("['401 Unauthorized']")
79 |
80 | @pytest.mark.asyncio
81 | async def test_digest_auth(self, db_request_data):
82 |
83 | response = await request_handler('httpbin-digest-auth-200')
84 | headers, _ = response.get('response')
85 |
86 | assert headers.startswith("['200 Ok']")
87 |
88 | response = await request_handler('httpbin-digest-auth-401')
89 | headers, _ = response.get('response')
90 |
91 | assert headers.startswith("['401 Unauthorized']")
92 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit.mouse_events import MouseEventType
2 |
3 |
4 | class SingleClick:
5 | def __init__(self):
6 | self.event_type = MouseEventType.MOUSE_UP
7 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py35,py36,py37
3 |
4 | [testenv]
5 | commands =
6 | pip install -q -e .[dev]
7 | pytest
8 |
--------------------------------------------------------------------------------