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 CHANGED
@@ -1 +1 @@
1
- __version__ = '0.1.2'
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,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 = 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
- 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.has_response = False
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.has_response = False
306
- else:
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 display_response(self, response: httpx.Response) -> None:
314
- def display_status() -> None:
315
- self.response_area.border_title = (
316
- 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,
317
240
  )
318
241
 
319
- def display_size_and_elapsed_time() -> None:
320
- 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
+ )
321
253
 
322
- def display_headers() -> None:
323
- for header_key, header_value in response.headers.multi_items():
324
- self.response_area.headers_data_table.add_row(
325
- 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'
326
274
  )
327
-
328
- def display_body() -> None:
329
- resp_content_type: str = response.headers.get('Content-Type', '')
330
- body_text_language = resp_content_type.rsplit('/')[1].lower()
331
- self.response_area.body_type_select.value = body_text_language
332
- self.response_area.body_text_area.language = body_text_language
333
- self.response_area.body_text_area.insert(response.text)
334
- self.response_area.body_text_area.scroll_home(
335
- 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,
336
318
  )
337
319
 
338
- display_status()
339
- display_size_and_elapsed_time()
340
- display_headers()
341
- 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
+ )