restiny 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- restiny/__about__.py +1 -1
- restiny/core/app.py +184 -177
- restiny/core/request_area.py +102 -100
- restiny/core/response_area.py +53 -20
- restiny/core/url_area.py +28 -20
- restiny/enums.py +0 -1
- restiny/widgets/__init__.py +2 -2
- restiny/widgets/dynamic_fields.py +375 -96
- restiny/widgets/path_chooser.py +71 -26
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/METADATA +5 -4
- restiny-0.2.1.dist-info/RECORD +24 -0
- restiny/assets/__pycache__/__init__.cpython-310.pyc +0 -0
- restiny/assets/__pycache__/__init__.cpython-313.pyc +0 -0
- restiny/assets/__pycache__/__init__.cpython-314.pyc +0 -0
- restiny/screens/__init__.py +0 -0
- restiny/screens/dialog.py +0 -109
- restiny-0.1.2.dist-info/RECORD +0 -29
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/WHEEL +0 -0
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/entry_points.txt +0 -0
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/top_level.txt +0 -0
restiny/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.1
|
|
1
|
+
__version__ = '0.2.1'
|
restiny/core/app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import mimetypes
|
|
4
|
+
from http import HTTPStatus
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
@@ -15,7 +16,14 @@ from textual.widgets import Footer, Header
|
|
|
15
16
|
|
|
16
17
|
from restiny.__about__ import __version__
|
|
17
18
|
from restiny.assets import STYLE_TCSS
|
|
18
|
-
from restiny.core import
|
|
19
|
+
from restiny.core import (
|
|
20
|
+
RequestArea,
|
|
21
|
+
RequestAreaData,
|
|
22
|
+
ResponseArea,
|
|
23
|
+
URLArea,
|
|
24
|
+
URLAreaData,
|
|
25
|
+
)
|
|
26
|
+
from restiny.core.response_area import ResponseAreaData
|
|
19
27
|
from restiny.enums import BodyMode, BodyRawLanguage, ContentType
|
|
20
28
|
from restiny.utils import build_curl_cmd
|
|
21
29
|
|
|
@@ -67,25 +75,6 @@ class RESTinyApp(App, inherit_bindings=False):
|
|
|
67
75
|
self.request_area = self.query_one(RequestArea)
|
|
68
76
|
self.response_area = self.query_one(ResponseArea)
|
|
69
77
|
|
|
70
|
-
@on(DescendantFocus)
|
|
71
|
-
def on_focus(self, event: DescendantFocus) -> None:
|
|
72
|
-
self.last_focused_widget = event.widget
|
|
73
|
-
last_focused_maximizable_area = self.find_maximizable_area_by_widget(
|
|
74
|
-
widget=event.widget
|
|
75
|
-
)
|
|
76
|
-
if last_focused_maximizable_area:
|
|
77
|
-
self.last_focused_maximizable_area = last_focused_maximizable_area
|
|
78
|
-
|
|
79
|
-
@on(URLArea.SendRequest)
|
|
80
|
-
def on_send_request(self, message: URLArea.SendRequest) -> None:
|
|
81
|
-
self.response_area.reset_response()
|
|
82
|
-
self.current_request = asyncio.create_task(self.send_request())
|
|
83
|
-
|
|
84
|
-
@on(URLArea.CancelRequest)
|
|
85
|
-
def on_cancel_request(self, message: URLArea.CancelRequest) -> None:
|
|
86
|
-
if self.current_request and not self.current_request.done():
|
|
87
|
-
self.current_request.cancel()
|
|
88
|
-
|
|
89
78
|
def action_maximize_or_minimize_area(self) -> None:
|
|
90
79
|
if self.screen.maximized:
|
|
91
80
|
self.screen.minimize()
|
|
@@ -144,39 +133,55 @@ class RESTinyApp(App, inherit_bindings=False):
|
|
|
144
133
|
form_multipart=form_multipart,
|
|
145
134
|
files=files,
|
|
146
135
|
)
|
|
147
|
-
self.
|
|
148
|
-
pyperclip.copy(curl_cmd)
|
|
136
|
+
self.copy_to_clipboard(curl_cmd)
|
|
149
137
|
self.notify(
|
|
150
|
-
|
|
138
|
+
'Command CURL copied to clipboard',
|
|
151
139
|
severity='information',
|
|
152
140
|
)
|
|
153
141
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
142
|
+
def copy_to_clipboard(self, text: str) -> None:
|
|
143
|
+
super().copy_to_clipboard(text)
|
|
144
|
+
try:
|
|
145
|
+
# Also copy to the system clipboard (outside of the app)
|
|
146
|
+
pyperclip.copy(text)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@on(DescendantFocus)
|
|
151
|
+
def _on_focus(self, event: DescendantFocus) -> None:
|
|
152
|
+
self.last_focused_widget = event.widget
|
|
153
|
+
last_focused_maximizable_area = self._find_maximizable_area_by_widget(
|
|
154
|
+
widget=event.widget
|
|
155
|
+
)
|
|
156
|
+
if last_focused_maximizable_area:
|
|
157
|
+
self.last_focused_maximizable_area = last_focused_maximizable_area
|
|
158
|
+
|
|
159
|
+
@on(URLArea.SendRequest)
|
|
160
|
+
def _on_send_request(self, message: URLArea.SendRequest) -> None:
|
|
161
|
+
self.current_request = asyncio.create_task(self._send_request())
|
|
162
|
+
|
|
163
|
+
@on(URLArea.CancelRequest)
|
|
164
|
+
def _on_cancel_request(self, message: URLArea.CancelRequest) -> None:
|
|
165
|
+
if self.current_request and not self.current_request.done():
|
|
166
|
+
self.current_request.cancel()
|
|
167
|
+
|
|
168
|
+
def _find_maximizable_area_by_widget(
|
|
169
|
+
self, widget: Widget
|
|
170
|
+
) -> Widget | None:
|
|
160
171
|
while widget is not None:
|
|
161
|
-
if
|
|
172
|
+
if (
|
|
173
|
+
isinstance(widget, URLArea)
|
|
174
|
+
or isinstance(widget, RequestArea)
|
|
175
|
+
or isinstance(widget, ResponseArea)
|
|
176
|
+
):
|
|
162
177
|
return widget
|
|
163
178
|
widget = widget.parent
|
|
164
179
|
|
|
165
|
-
async def
|
|
180
|
+
async def _send_request(self) -> None:
|
|
166
181
|
url_area_data = self.url_area.get_data()
|
|
167
182
|
request_area_data = self.request_area.get_data()
|
|
168
183
|
|
|
169
|
-
|
|
170
|
-
header.key: header.value
|
|
171
|
-
for header in request_area_data.headers
|
|
172
|
-
if header.enabled
|
|
173
|
-
}
|
|
174
|
-
query_params: dict[str, str] = {
|
|
175
|
-
param.key: param.value
|
|
176
|
-
for param in request_area_data.query_params
|
|
177
|
-
if param.enabled
|
|
178
|
-
}
|
|
179
|
-
|
|
184
|
+
self.response_area.set_data(data=None)
|
|
180
185
|
self.response_area.loading = True
|
|
181
186
|
self.url_area.request_pending = True
|
|
182
187
|
try:
|
|
@@ -185,114 +190,14 @@ class RESTinyApp(App, inherit_bindings=False):
|
|
|
185
190
|
follow_redirects=request_area_data.options.follow_redirects,
|
|
186
191
|
verify=request_area_data.options.verify_ssl,
|
|
187
192
|
) as http_client:
|
|
188
|
-
request =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
url=url_area_data.url,
|
|
194
|
-
headers=headers,
|
|
195
|
-
params=query_params,
|
|
196
|
-
)
|
|
197
|
-
else:
|
|
198
|
-
if request_area_data.body.type == BodyMode.RAW:
|
|
199
|
-
raw = request_area_data.body.payload
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
request_area_data.body.raw_language
|
|
203
|
-
== BodyRawLanguage.JSON
|
|
204
|
-
):
|
|
205
|
-
headers['content-type'] = ContentType.JSON
|
|
206
|
-
try:
|
|
207
|
-
raw = json.dumps(raw)
|
|
208
|
-
except Exception:
|
|
209
|
-
pass
|
|
210
|
-
elif (
|
|
211
|
-
request_area_data.body.raw_language
|
|
212
|
-
== BodyRawLanguage.YAML
|
|
213
|
-
):
|
|
214
|
-
headers['content-type'] = ContentType.YAML
|
|
215
|
-
elif (
|
|
216
|
-
request_area_data.body.raw_language
|
|
217
|
-
== BodyRawLanguage.HTML
|
|
218
|
-
):
|
|
219
|
-
headers['content-type'] = ContentType.HTML
|
|
220
|
-
elif (
|
|
221
|
-
request_area_data.body.raw_language
|
|
222
|
-
== BodyRawLanguage.XML
|
|
223
|
-
):
|
|
224
|
-
headers['content-type'] = ContentType.XML
|
|
225
|
-
elif (
|
|
226
|
-
request_area_data.body.raw_language
|
|
227
|
-
== BodyRawLanguage.PLAIN
|
|
228
|
-
):
|
|
229
|
-
headers['content-type'] = ContentType.TEXT
|
|
230
|
-
|
|
231
|
-
request = http_client.build_request(
|
|
232
|
-
method=url_area_data.method,
|
|
233
|
-
url=url_area_data.url,
|
|
234
|
-
headers=headers,
|
|
235
|
-
params=query_params,
|
|
236
|
-
content=raw,
|
|
237
|
-
)
|
|
238
|
-
elif request_area_data.body.type == BodyMode.FILE:
|
|
239
|
-
file = request_area_data.body.payload
|
|
240
|
-
headers['content-type'] = (
|
|
241
|
-
mimetypes.guess_type(file.name)[0]
|
|
242
|
-
or 'application/octet-stream'
|
|
243
|
-
)
|
|
244
|
-
request = http_client.build_request(
|
|
245
|
-
method=url_area_data.method,
|
|
246
|
-
url=url_area_data.url,
|
|
247
|
-
headers=headers,
|
|
248
|
-
params=query_params,
|
|
249
|
-
content=file.read_bytes(),
|
|
250
|
-
)
|
|
251
|
-
elif (
|
|
252
|
-
request_area_data.body.type == BodyMode.FORM_URLENCODED
|
|
253
|
-
):
|
|
254
|
-
form_urlencoded = {
|
|
255
|
-
form_item.key: form_item.value
|
|
256
|
-
for form_item in request_area_data.body.payload
|
|
257
|
-
if form_item.enabled
|
|
258
|
-
}
|
|
259
|
-
request = http_client.build_request(
|
|
260
|
-
method=url_area_data.method,
|
|
261
|
-
url=url_area_data.url,
|
|
262
|
-
headers=headers,
|
|
263
|
-
params=query_params,
|
|
264
|
-
data=form_urlencoded,
|
|
265
|
-
)
|
|
266
|
-
elif (
|
|
267
|
-
request_area_data.body.type == BodyMode.FORM_MULTIPART
|
|
268
|
-
):
|
|
269
|
-
form_multipart_str = {
|
|
270
|
-
form_item.key: form_item.value
|
|
271
|
-
for form_item in request_area_data.body.payload
|
|
272
|
-
if form_item.enabled
|
|
273
|
-
and isinstance(form_item.value, str)
|
|
274
|
-
}
|
|
275
|
-
form_multipart_files = {
|
|
276
|
-
form_item.key: (
|
|
277
|
-
form_item.value.name,
|
|
278
|
-
form_item.value.read_bytes(),
|
|
279
|
-
mimetypes.guess_type(form_item.value.name)[0]
|
|
280
|
-
or 'application/octet-stream',
|
|
281
|
-
)
|
|
282
|
-
for form_item in request_area_data.body.payload
|
|
283
|
-
if form_item.enabled
|
|
284
|
-
and isinstance(form_item.value, Path)
|
|
285
|
-
}
|
|
286
|
-
request = http_client.build_request(
|
|
287
|
-
method=url_area_data.method,
|
|
288
|
-
url=url_area_data.url,
|
|
289
|
-
headers=headers,
|
|
290
|
-
params=query_params,
|
|
291
|
-
data=form_multipart_str,
|
|
292
|
-
files=form_multipart_files,
|
|
293
|
-
)
|
|
294
|
-
|
|
193
|
+
request = self._build_request(
|
|
194
|
+
http_client=http_client,
|
|
195
|
+
url_area_data=url_area_data,
|
|
196
|
+
request_area_data=request_area_data,
|
|
197
|
+
)
|
|
295
198
|
response = await http_client.send(request=request)
|
|
199
|
+
self._display_response(response=response)
|
|
200
|
+
self.response_area.is_showing_response = True
|
|
296
201
|
except httpx.RequestError as error:
|
|
297
202
|
error_name = type(error).__name__
|
|
298
203
|
error_message = str(error)
|
|
@@ -300,42 +205,144 @@ class RESTinyApp(App, inherit_bindings=False):
|
|
|
300
205
|
self.notify(f'{error_name}: {error_message}', severity='error')
|
|
301
206
|
else:
|
|
302
207
|
self.notify(f'{error_name}', severity='error')
|
|
303
|
-
self.response_area.
|
|
208
|
+
self.response_area.set_data(data=None)
|
|
209
|
+
self.response_area.is_showing_response = False
|
|
304
210
|
except asyncio.CancelledError:
|
|
305
|
-
self.response_area.
|
|
306
|
-
|
|
307
|
-
self.display_response(response=response)
|
|
308
|
-
self.response_area.has_response = True
|
|
211
|
+
self.response_area.set_data(data=None)
|
|
212
|
+
self.response_area.is_showing_response = False
|
|
309
213
|
finally:
|
|
310
214
|
self.response_area.loading = False
|
|
311
215
|
self.url_area.request_pending = False
|
|
312
216
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
217
|
+
def _build_request(
|
|
218
|
+
self,
|
|
219
|
+
http_client: httpx.Client,
|
|
220
|
+
url_area_data: URLAreaData,
|
|
221
|
+
request_area_data: RequestAreaData,
|
|
222
|
+
) -> httpx.Request:
|
|
223
|
+
headers: dict[str, str] = {
|
|
224
|
+
header.key: header.value
|
|
225
|
+
for header in request_area_data.headers
|
|
226
|
+
if header.enabled
|
|
227
|
+
}
|
|
228
|
+
query_params: dict[str, str] = {
|
|
229
|
+
param.key: param.value
|
|
230
|
+
for param in request_area_data.query_params
|
|
231
|
+
if param.enabled
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if not request_area_data.body.enabled:
|
|
235
|
+
return http_client.build_request(
|
|
236
|
+
method=url_area_data.method,
|
|
237
|
+
url=url_area_data.url,
|
|
238
|
+
headers=headers,
|
|
239
|
+
params=query_params,
|
|
317
240
|
)
|
|
318
241
|
|
|
319
|
-
|
|
320
|
-
|
|
242
|
+
if request_area_data.body.type == BodyMode.RAW:
|
|
243
|
+
raw_language_to_content_type = {
|
|
244
|
+
BodyRawLanguage.JSON: ContentType.JSON,
|
|
245
|
+
BodyRawLanguage.YAML: ContentType.YAML,
|
|
246
|
+
BodyRawLanguage.HTML: ContentType.HTML,
|
|
247
|
+
BodyRawLanguage.XML: ContentType.XML,
|
|
248
|
+
BodyRawLanguage.PLAIN: ContentType.TEXT,
|
|
249
|
+
}
|
|
250
|
+
headers['content-type'] = raw_language_to_content_type.get(
|
|
251
|
+
request_area_data.body.raw_language, ContentType.TEXT
|
|
252
|
+
)
|
|
321
253
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
254
|
+
raw = request_area_data.body.payload
|
|
255
|
+
if headers['content-type'] == ContentType.JSON:
|
|
256
|
+
try:
|
|
257
|
+
raw = json.dumps(raw)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
return http_client.build_request(
|
|
262
|
+
method=url_area_data.method,
|
|
263
|
+
url=url_area_data.url,
|
|
264
|
+
headers=headers,
|
|
265
|
+
params=query_params,
|
|
266
|
+
content=raw,
|
|
267
|
+
)
|
|
268
|
+
elif request_area_data.body.type == BodyMode.FILE:
|
|
269
|
+
file = request_area_data.body.payload
|
|
270
|
+
if 'content-type' not in headers:
|
|
271
|
+
headers['content-type'] = (
|
|
272
|
+
mimetypes.guess_type(file.name)[0]
|
|
273
|
+
or 'application/octet-stream'
|
|
326
274
|
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
275
|
+
return http_client.build_request(
|
|
276
|
+
method=url_area_data.method,
|
|
277
|
+
url=url_area_data.url,
|
|
278
|
+
headers=headers,
|
|
279
|
+
params=query_params,
|
|
280
|
+
content=file.read_bytes(),
|
|
281
|
+
)
|
|
282
|
+
elif request_area_data.body.type == BodyMode.FORM_URLENCODED:
|
|
283
|
+
form_urlencoded = {
|
|
284
|
+
form_item.key: form_item.value
|
|
285
|
+
for form_item in request_area_data.body.payload
|
|
286
|
+
if form_item.enabled
|
|
287
|
+
}
|
|
288
|
+
return http_client.build_request(
|
|
289
|
+
method=url_area_data.method,
|
|
290
|
+
url=url_area_data.url,
|
|
291
|
+
headers=headers,
|
|
292
|
+
params=query_params,
|
|
293
|
+
data=form_urlencoded,
|
|
294
|
+
)
|
|
295
|
+
elif request_area_data.body.type == BodyMode.FORM_MULTIPART:
|
|
296
|
+
form_multipart_str = {
|
|
297
|
+
form_item.key: form_item.value
|
|
298
|
+
for form_item in request_area_data.body.payload
|
|
299
|
+
if form_item.enabled and isinstance(form_item.value, str)
|
|
300
|
+
}
|
|
301
|
+
form_multipart_files = {
|
|
302
|
+
form_item.key: (
|
|
303
|
+
form_item.value.name,
|
|
304
|
+
form_item.value.read_bytes(),
|
|
305
|
+
mimetypes.guess_type(form_item.value.name)[0]
|
|
306
|
+
or 'application/octet-stream',
|
|
307
|
+
)
|
|
308
|
+
for form_item in request_area_data.body.payload
|
|
309
|
+
if form_item.enabled and isinstance(form_item.value, Path)
|
|
310
|
+
}
|
|
311
|
+
return http_client.build_request(
|
|
312
|
+
method=url_area_data.method,
|
|
313
|
+
url=url_area_data.url,
|
|
314
|
+
headers=headers,
|
|
315
|
+
params=query_params,
|
|
316
|
+
data=form_multipart_str,
|
|
317
|
+
files=form_multipart_files,
|
|
336
318
|
)
|
|
337
319
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
320
|
+
def _display_response(self, response: httpx.Response) -> None:
|
|
321
|
+
status = HTTPStatus(response.status_code)
|
|
322
|
+
size = response.num_bytes_downloaded
|
|
323
|
+
elapsed_time = round(response.elapsed.total_seconds(), 2)
|
|
324
|
+
headers = {
|
|
325
|
+
header_key: header_value
|
|
326
|
+
for header_key, header_value in response.headers.multi_items()
|
|
327
|
+
}
|
|
328
|
+
content_type_to_body_language = {
|
|
329
|
+
ContentType.TEXT: BodyRawLanguage.PLAIN,
|
|
330
|
+
ContentType.HTML: BodyRawLanguage.HTML,
|
|
331
|
+
ContentType.JSON: BodyRawLanguage.JSON,
|
|
332
|
+
ContentType.YAML: BodyRawLanguage.YAML,
|
|
333
|
+
ContentType.XML: BodyRawLanguage.XML,
|
|
334
|
+
}
|
|
335
|
+
body_raw_language = content_type_to_body_language.get(
|
|
336
|
+
response.headers.get('Content-Type'), BodyRawLanguage.PLAIN
|
|
337
|
+
)
|
|
338
|
+
body_raw = response.text
|
|
339
|
+
self.response_area.set_data(
|
|
340
|
+
data=ResponseAreaData(
|
|
341
|
+
status=status,
|
|
342
|
+
size=size,
|
|
343
|
+
elapsed_time=elapsed_time,
|
|
344
|
+
headers=headers,
|
|
345
|
+
body_raw_language=body_raw_language,
|
|
346
|
+
body_raw=body_raw,
|
|
347
|
+
)
|
|
348
|
+
)
|