restiny 0.2.0__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 CHANGED
@@ -1 +1 @@
1
- __version__ = '0.2.0'
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 RequestArea, ResponseArea, URLArea
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.app.copy_to_clipboard(curl_cmd)
148
- pyperclip.copy(curl_cmd)
136
+ self.copy_to_clipboard(curl_cmd)
149
137
  self.notify(
150
- f'Command cURL copied to clipboard: `{curl_cmd}`',
138
+ 'Command CURL copied to clipboard',
151
139
  severity='information',
152
140
  )
153
141
 
154
- def find_maximizable_area_by_widget(self, widget: Widget) -> Widget | None:
155
- maximizable_areas: list[str] = [
156
- URLArea.__name__,
157
- RequestArea.__name__,
158
- ResponseArea.__name__,
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 widget.__class__.__name__ in maximizable_areas:
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 send_request(self) -> None:
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
- headers: dict[str, str] = {
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,115 +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 = None
189
-
190
- if not request_area_data.body.enabled:
191
- request = http_client.build_request(
192
- method=url_area_data.method,
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
- if 'content-type' not in headers:
241
- headers['content-type'] = (
242
- mimetypes.guess_type(file.name)[0]
243
- or 'application/octet-stream'
244
- )
245
- request = http_client.build_request(
246
- method=url_area_data.method,
247
- url=url_area_data.url,
248
- headers=headers,
249
- params=query_params,
250
- content=file.read_bytes(),
251
- )
252
- elif (
253
- request_area_data.body.type == BodyMode.FORM_URLENCODED
254
- ):
255
- form_urlencoded = {
256
- form_item.key: form_item.value
257
- for form_item in request_area_data.body.payload
258
- if form_item.enabled
259
- }
260
- request = http_client.build_request(
261
- method=url_area_data.method,
262
- url=url_area_data.url,
263
- headers=headers,
264
- params=query_params,
265
- data=form_urlencoded,
266
- )
267
- elif (
268
- request_area_data.body.type == BodyMode.FORM_MULTIPART
269
- ):
270
- form_multipart_str = {
271
- form_item.key: form_item.value
272
- for form_item in request_area_data.body.payload
273
- if form_item.enabled
274
- and isinstance(form_item.value, str)
275
- }
276
- form_multipart_files = {
277
- form_item.key: (
278
- form_item.value.name,
279
- form_item.value.read_bytes(),
280
- mimetypes.guess_type(form_item.value.name)[0]
281
- or 'application/octet-stream',
282
- )
283
- for form_item in request_area_data.body.payload
284
- if form_item.enabled
285
- and isinstance(form_item.value, Path)
286
- }
287
- request = http_client.build_request(
288
- method=url_area_data.method,
289
- url=url_area_data.url,
290
- headers=headers,
291
- params=query_params,
292
- data=form_multipart_str,
293
- files=form_multipart_files,
294
- )
295
-
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
+ )
296
198
  response = await http_client.send(request=request)
199
+ self._display_response(response=response)
200
+ self.response_area.is_showing_response = True
297
201
  except httpx.RequestError as error:
298
202
  error_name = type(error).__name__
299
203
  error_message = str(error)
@@ -301,42 +205,144 @@ class RESTinyApp(App, inherit_bindings=False):
301
205
  self.notify(f'{error_name}: {error_message}', severity='error')
302
206
  else:
303
207
  self.notify(f'{error_name}', severity='error')
304
- self.response_area.has_response = False
208
+ self.response_area.set_data(data=None)
209
+ self.response_area.is_showing_response = False
305
210
  except asyncio.CancelledError:
306
- self.response_area.has_response = False
307
- else:
308
- self.display_response(response=response)
309
- self.response_area.has_response = True
211
+ self.response_area.set_data(data=None)
212
+ self.response_area.is_showing_response = False
310
213
  finally:
311
214
  self.response_area.loading = False
312
215
  self.url_area.request_pending = False
313
216
 
314
- def display_response(self, response: httpx.Response) -> None:
315
- def display_status() -> None:
316
- self.response_area.border_title = (
317
- f'Response - {response.status_code} {response.reason_phrase}'
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,
318
240
  )
319
241
 
320
- def display_size_and_elapsed_time() -> None:
321
- self.response_area.border_subtitle = f'{response.num_bytes_downloaded} bytes in {response.elapsed.total_seconds():.2f} seconds'
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
+ )
322
253
 
323
- def display_headers() -> None:
324
- for header_key, header_value in response.headers.multi_items():
325
- self.response_area.headers_data_table.add_row(
326
- header_key, header_value
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'
327
274
  )
328
-
329
- def display_body() -> None:
330
- resp_content_type: str = response.headers.get('Content-Type', '')
331
- body_text_language = resp_content_type.rsplit('/')[1].lower()
332
- self.response_area.body_type_select.value = body_text_language
333
- self.response_area.body_text_area.language = body_text_language
334
- self.response_area.body_text_area.insert(response.text)
335
- self.response_area.body_text_area.scroll_home(
336
- animate=False, force=True, immediate=True
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,
337
318
  )
338
319
 
339
- display_status()
340
- display_size_and_elapsed_time()
341
- display_headers()
342
- display_body()
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
+ )
@@ -174,6 +174,7 @@ class RequestArea(Static):
174
174
  yield Input(
175
175
  '5.5',
176
176
  placeholder='5.5',
177
+ select_on_focus=False,
177
178
  id='options-timeout',
178
179
  classes='w-1fr',
179
180
  )
@@ -214,8 +215,16 @@ class RequestArea(Static):
214
215
  '#options-verify-ssl', Switch
215
216
  )
216
217
 
218
+ def get_data(self) -> RequestAreaData:
219
+ return RequestAreaData(
220
+ headers=self._get_headers(),
221
+ query_params=self._get_query_params(),
222
+ body=self._get_body(),
223
+ options=self._get_options(),
224
+ )
225
+
217
226
  @on(Select.Changed, '#body-mode')
218
- def on_change_body_type(self, message: Select.Changed) -> None:
227
+ def _on_change_body_type(self, message: Select.Changed) -> None:
219
228
  if message.value == BodyMode.FILE:
220
229
  self.body_mode_switcher.current = 'body-mode-file'
221
230
  elif message.value == BodyMode.RAW:
@@ -226,27 +235,27 @@ class RequestArea(Static):
226
235
  self.body_mode_switcher.current = 'body-mode-form-multipart'
227
236
 
228
237
  @on(Select.Changed, '#body-raw-language')
229
- def on_change_body_text_language(self, message: Select.Changed) -> None:
238
+ def _on_change_body_raw_language(self, message: Select.Changed) -> None:
230
239
  self.body_raw_editor.language = message.value
231
240
 
232
241
  @on(DynamicFields.FieldFilled, '#body-form-urlencoded')
233
- def on_form_filled(self, message: DynamicFields.FieldFilled) -> None:
242
+ def _on_form_filled(self, message: DynamicFields.FieldFilled) -> None:
234
243
  self.body_enabled_switch.value = True
235
244
 
236
245
  @on(DynamicFields.FieldEmpty, '#body-form-urlencoded')
237
- def on_form_empty(self, message: DynamicFields.FieldEmpty) -> None:
246
+ def _on_form_empty(self, message: DynamicFields.FieldEmpty) -> None:
238
247
  if not message.control.filled_fields:
239
248
  self.body_enabled_switch.value = False
240
249
 
241
250
  @on(CustomTextArea.Changed, '#body-raw')
242
- def on_change_body_text(self, message: CustomTextArea.Changed) -> None:
251
+ def _on_change_body_raw(self, message: CustomTextArea.Changed) -> None:
243
252
  if self.body_raw_editor.text == '':
244
253
  self.body_enabled_switch.value = False
245
254
  else:
246
255
  self.body_enabled_switch.value = True
247
256
 
248
257
  @on(Input.Changed, '#options-timeout')
249
- def on_change_timeout(self, message: Input.Changed) -> None:
258
+ def _on_change_timeout(self, message: Input.Changed) -> None:
250
259
  new_value = message.value
251
260
 
252
261
  if new_value == '':
@@ -259,80 +268,70 @@ class RequestArea(Static):
259
268
  self.options_timeout_input.value[:-1]
260
269
  )
261
270
 
262
- def get_data(self) -> RequestAreaData:
263
- def get_headers() -> list[HeaderField]:
264
- return [
265
- HeaderField(
266
- enabled=header_field['enabled'],
267
- key=header_field['key'],
268
- value=header_field['value'],
269
- )
270
- for header_field in self.header_fields.values
271
- ]
272
-
273
- def get_query_params() -> list[QueryParamField]:
274
- return [
275
- QueryParamField(
276
- enabled=query_param_field['enabled'],
277
- key=query_param_field['key'],
278
- value=query_param_field['value'],
279
- )
280
- for query_param_field in self.param_fields.values
281
- ]
282
-
283
- def get_body() -> RequestAreaData.Body:
284
- body_send: bool = self.body_enabled_switch.value
285
- body_type: str = BodyMode(self.body_mode_select.value)
286
-
287
- payload = None
288
- if body_type == BodyMode.RAW:
289
- payload = self.body_raw_editor.text
290
- elif body_type == BodyMode.FILE:
291
- payload = self.body_file_path_chooser.path
292
- elif body_type == BodyMode.FORM_URLENCODED:
293
- payload = []
294
- for form_item in self.body_form_urlencoded_fields.values:
295
- payload.append(
296
- FormUrlEncodedField(
297
- enabled=form_item['enabled'],
298
- key=form_item['key'],
299
- value=form_item['value'],
300
- )
271
+ def _get_headers(self) -> list[HeaderField]:
272
+ return [
273
+ HeaderField(
274
+ enabled=header_field['enabled'],
275
+ key=header_field['key'],
276
+ value=header_field['value'],
277
+ )
278
+ for header_field in self.header_fields.values
279
+ ]
280
+
281
+ def _get_query_params(self) -> list[QueryParamField]:
282
+ return [
283
+ QueryParamField(
284
+ enabled=query_param_field['enabled'],
285
+ key=query_param_field['key'],
286
+ value=query_param_field['value'],
287
+ )
288
+ for query_param_field in self.param_fields.values
289
+ ]
290
+
291
+ def _get_body(self) -> RequestAreaData.Body:
292
+ body_send: bool = self.body_enabled_switch.value
293
+ body_type: str = BodyMode(self.body_mode_select.value)
294
+
295
+ payload = None
296
+ if body_type == BodyMode.RAW:
297
+ payload = self.body_raw_editor.text
298
+ elif body_type == BodyMode.FILE:
299
+ payload = self.body_file_path_chooser.path
300
+ elif body_type == BodyMode.FORM_URLENCODED:
301
+ payload = []
302
+ for form_item in self.body_form_urlencoded_fields.values:
303
+ payload.append(
304
+ FormUrlEncodedField(
305
+ enabled=form_item['enabled'],
306
+ key=form_item['key'],
307
+ value=form_item['value'],
301
308
  )
302
- elif body_type == BodyMode.FORM_MULTIPART:
303
- payload = []
304
- for form_item in self.body_form_multipart_fields.values:
305
- payload.append(
306
- FormMultipartField(
307
- enabled=form_item['enabled'],
308
- key=form_item['key'],
309
- value=form_item['value'],
310
- )
309
+ )
310
+ elif body_type == BodyMode.FORM_MULTIPART:
311
+ payload = []
312
+ for form_item in self.body_form_multipart_fields.values:
313
+ payload.append(
314
+ FormMultipartField(
315
+ enabled=form_item['enabled'],
316
+ key=form_item['key'],
317
+ value=form_item['value'],
311
318
  )
319
+ )
312
320
 
313
- return RequestAreaData.Body(
314
- enabled=body_send,
315
- raw_language=BodyRawLanguage(
316
- self.body_raw_language_select.value
317
- ),
318
- type=body_type,
319
- payload=payload,
320
- )
321
-
322
- def get_options() -> RequestAreaData.Options:
323
- timeout = None
324
- if self.options_timeout_input.value:
325
- timeout = float(self.options_timeout_input.value)
321
+ return RequestAreaData.Body(
322
+ enabled=body_send,
323
+ raw_language=BodyRawLanguage(self.body_raw_language_select.value),
324
+ type=body_type,
325
+ payload=payload,
326
+ )
326
327
 
327
- return RequestAreaData.Options(
328
- timeout=timeout,
329
- follow_redirects=self.options_follow_redirects_switch.value,
330
- verify_ssl=self.options_verify_ssl_switch.value,
331
- )
328
+ def _get_options(self) -> RequestAreaData.Options:
329
+ timeout = None
330
+ if self.options_timeout_input.value:
331
+ timeout = float(self.options_timeout_input.value)
332
332
 
333
- return RequestAreaData(
334
- headers=get_headers(),
335
- query_params=get_query_params(),
336
- body=get_body(),
337
- options=get_options(),
333
+ return RequestAreaData.Options(
334
+ timeout=timeout,
335
+ follow_redirects=self.options_follow_redirects_switch.value,
336
+ verify_ssl=self.options_verify_ssl_switch.value,
338
337
  )