restiny 0.2.1__py3-none-any.whl → 0.5.0__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.
@@ -0,0 +1,161 @@
1
+ from enum import StrEnum
2
+
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Horizontal
6
+ from textual.message import Message
7
+ from textual.widget import Widget
8
+ from textual.widgets import Button
9
+
10
+ from restiny.widgets import CustomInput
11
+
12
+
13
+ class _Icon(StrEnum):
14
+ SHOW = ' 🔓 '
15
+ HIDE = ' 🔒 '
16
+
17
+
18
+ class _Tooltip(StrEnum):
19
+ SHOW = 'Show'
20
+ HIDE = 'Hide'
21
+
22
+
23
+ class PasswordInput(Widget):
24
+ DEFAULT_CSS = """
25
+ PasswordInput {
26
+ width: 1fr;
27
+ height: auto;
28
+ }
29
+
30
+ PasswordInput > Horizontal {
31
+ width: auto;
32
+ height: auto;
33
+ }
34
+
35
+ PasswordInput CustomInput {
36
+ width: 1fr;
37
+ margin-right: 0;
38
+ border-right: none;
39
+ }
40
+
41
+ PasswordInput CustomInput:focus {
42
+ border-right: none;
43
+ }
44
+
45
+
46
+ PasswordInput Button {
47
+ width: auto;
48
+ margin-left: 0;
49
+ border-left: none;
50
+ }
51
+
52
+ """
53
+
54
+ class Changed(Message):
55
+ """
56
+ Sent when value changed.
57
+ """
58
+
59
+ def __init__(self, input: 'PasswordInput', value: str):
60
+ super().__init__()
61
+ self.input = input
62
+ self.value = value
63
+
64
+ @property
65
+ def control(self) -> 'PasswordInput':
66
+ return self.input
67
+
68
+ class Shown(Message):
69
+ """
70
+ Sent when the value becomes visible.
71
+ """
72
+
73
+ def __init__(self, input: 'PasswordInput') -> None:
74
+ super().__init__()
75
+ self.input = input
76
+
77
+ @property
78
+ def control(self) -> 'PasswordInput':
79
+ return self.input
80
+
81
+ class Hidden(Message):
82
+ """
83
+ Sent when the value becomes hidden.
84
+ """
85
+
86
+ def __init__(self, input: 'PasswordInput') -> None:
87
+ super().__init__()
88
+ self.input = input
89
+
90
+ @property
91
+ def control(self) -> 'PasswordInput':
92
+ return self.input
93
+
94
+ def __init__(self, *args, **kwargs) -> None:
95
+ super().__init__(
96
+ id=kwargs.pop('id', None), classes=kwargs.pop('classes', None)
97
+ )
98
+ kwargs.pop('password', None)
99
+ self._input_args = args
100
+ self._input_kwargs = kwargs
101
+
102
+ def compose(self) -> ComposeResult:
103
+ with Horizontal():
104
+ yield CustomInput(
105
+ *self._input_args,
106
+ **self._input_kwargs,
107
+ password=True,
108
+ id='value',
109
+ )
110
+ yield Button(
111
+ _Icon.SHOW, tooltip=_Tooltip.SHOW, id='toggle-visibility'
112
+ )
113
+
114
+ def on_mount(self) -> None:
115
+ self.value_input = self.query_one('#value', CustomInput)
116
+ self.toggle_visibility_button = self.query_one(
117
+ '#toggle-visibility', Button
118
+ )
119
+
120
+ def show(self) -> None:
121
+ self.value_input.password = False
122
+ self.toggle_visibility_button.label = _Icon.HIDE
123
+ self.toggle_visibility_button.tooltip = _Tooltip.HIDE
124
+ self.post_message(message=self.Hidden(input=self))
125
+
126
+ def hide(self) -> None:
127
+ self.value_input.password = True
128
+ self.toggle_visibility_button.label = _Icon.SHOW
129
+ self.toggle_visibility_button.tooltip = _Tooltip.SHOW
130
+ self.post_message(message=self.Shown(input=self))
131
+
132
+ @property
133
+ def value(self) -> str:
134
+ return self.value_input.value
135
+
136
+ @value.setter
137
+ def value(self, value: str) -> None:
138
+ self.value_input.value = value
139
+
140
+ @property
141
+ def shown(self) -> bool:
142
+ return self.value_input.password is False
143
+
144
+ @property
145
+ def hidden(self) -> bool:
146
+ return not self.shown
147
+
148
+ @on(CustomInput.Changed, '#value')
149
+ def _on_value_changed(self, message: CustomInput.Changed) -> None:
150
+ self.post_message(
151
+ message=self.Changed(input=self, value=message.value)
152
+ )
153
+
154
+ @on(Button.Pressed, '#toggle-visibility')
155
+ def _on_toggle_visibility(self, message: Button.Pressed) -> None:
156
+ if self.value_input.password is False:
157
+ self.hide()
158
+ elif self.value_input.password is True:
159
+ self.show()
160
+
161
+ self.value_input.focus()
@@ -9,10 +9,10 @@ from textual.message import Message
9
9
  from textual.reactive import Reactive
10
10
  from textual.screen import ModalScreen
11
11
  from textual.widget import Widget
12
- from textual.widgets import Button, Input, Label, Switch
12
+ from textual.widgets import Button, Label, Switch
13
13
 
14
14
  from restiny.utils import filter_paths
15
- from restiny.widgets import CustomDirectoryTree
15
+ from restiny.widgets import CustomDirectoryTree, CustomInput
16
16
 
17
17
 
18
18
  class PathChooserScreen(ModalScreen):
@@ -53,7 +53,7 @@ class PathChooserScreen(ModalScreen):
53
53
  yield CustomDirectoryTree(path='/')
54
54
 
55
55
  with Horizontal(classes='w-auto h-auto mt-1'):
56
- yield Input(
56
+ yield CustomInput(
57
57
  placeholder='--empty--',
58
58
  select_on_focus=False,
59
59
  disabled=True,
@@ -74,7 +74,7 @@ class PathChooserScreen(ModalScreen):
74
74
  '#option-show-hidden-dirs'
75
75
  )
76
76
  self.directory_tree = self.query_one(CustomDirectoryTree)
77
- self.input = self.query_one(Input)
77
+ self.input = self.query_one(CustomInput)
78
78
  self.btn_cancel = self.query_one('#cancel')
79
79
  self.btn_confirm = self.query_one('#choose')
80
80
 
@@ -197,7 +197,7 @@ class PathChooser(Widget):
197
197
  grid-columns: 1fr auto;
198
198
  }
199
199
 
200
- PathChooser > Input {
200
+ PathChooser > CustomInput {
201
201
  margin-right: 0;
202
202
  border-right: none;
203
203
  }
@@ -214,7 +214,7 @@ class PathChooser(Widget):
214
214
  """
215
215
 
216
216
  def __init__(
217
- self, path_chooser: PathChooser, path: Path | None
217
+ self, path_chooser: 'PathChooser', path: Path | None
218
218
  ) -> None:
219
219
  super().__init__()
220
220
  self.path_chooser = path_chooser
@@ -241,7 +241,7 @@ class PathChooser(Widget):
241
241
  ) -> None:
242
242
  super().__init__(*args, **kwargs)
243
243
  self.path_type = path_type
244
- self._initial_path = path
244
+ self._path = path
245
245
 
246
246
  def compose(self) -> ComposeResult:
247
247
  icon = ''
@@ -250,8 +250,8 @@ class PathChooser(Widget):
250
250
  elif self.path_type == _PathType.DIR:
251
251
  icon = ' 🗂 '
252
252
 
253
- yield Input(
254
- self._initial_path,
253
+ yield CustomInput(
254
+ str(self._path) if self._path else '',
255
255
  placeholder='--empty--',
256
256
  select_on_focus=False,
257
257
  disabled=True,
@@ -260,7 +260,7 @@ class PathChooser(Widget):
260
260
  yield Button(icon, tooltip=f'Choose {self.path_type}', id='choose')
261
261
 
262
262
  def on_mount(self) -> None:
263
- self.path_input = self.query_one('#path', Input)
263
+ self.path_input = self.query_one('#path', CustomInput)
264
264
  self.choose_button = self.query_one('#choose', Button)
265
265
 
266
266
  @property
@@ -275,8 +275,8 @@ class PathChooser(Widget):
275
275
  self.path_input.value = value
276
276
  self.path_input.tooltip = value
277
277
 
278
- @on(Input.Changed, '#path')
279
- def _on_path_changed(self, message: Input.Changed) -> None:
278
+ @on(CustomInput.Changed, '#path')
279
+ def _on_path_changed(self, message: CustomInput.Changed) -> None:
280
280
  self.post_message(
281
281
  message=self.Changed(path_chooser=self, path=self.path)
282
282
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: restiny
3
- Version: 0.2.1
3
+ Version: 0.5.0
4
4
  Summary: A minimalist HTTP client, no bullshit
5
5
  Author-email: Kalebe Chimanski de Almeida <kalebe.chi.almeida@gmail.com>
6
6
  License: Apache License
@@ -216,7 +216,6 @@ Classifier: License :: OSI Approved :: Apache Software License
216
216
  Classifier: Environment :: Console
217
217
  Classifier: Typing :: Typed
218
218
  Classifier: Programming Language :: Python :: 3
219
- Classifier: Programming Language :: Python :: 3.10
220
219
  Classifier: Programming Language :: Python :: 3.11
221
220
  Classifier: Programming Language :: Python :: 3.12
222
221
  Classifier: Programming Language :: Python :: 3.13
@@ -228,17 +227,20 @@ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
228
227
  Classifier: Topic :: Internet :: WWW/HTTP
229
228
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
230
229
  Classifier: Natural Language :: English
231
- Requires-Python: >=3.10
230
+ Requires-Python: >=3.11
232
231
  Description-Content-Type: text/markdown
233
232
  License-File: LICENSE
234
233
  Requires-Dist: textual<6.4,>=6.3
235
234
  Requires-Dist: textual[syntax]
236
235
  Requires-Dist: httpx<0.29,>=0.28
237
236
  Requires-Dist: pyperclip<1.10,>=1.9
237
+ Requires-Dist: sqlalchemy<3.0,>=2.0
238
+ Requires-Dist: pydantic<2.13,>=2.12
238
239
  Dynamic: license-file
239
240
 
240
241
  ![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red)
241
- ![Python versions](https://img.shields.io/badge/Python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue)
242
+ ![Python versions](https://img.shields.io/badge/Python-3.11%20|%203.12%20|%203.13%20|%203.14-blue)
243
+
242
244
 
243
245
 
244
246
  - [RESTiny](#restiny)
@@ -254,7 +256,7 @@ Dynamic: license-file
254
256
 
255
257
  _A minimal, elegant HTTP client for Python — with a TUI interface powered by [Textual](https://github.com/Textualize/textual)._
256
258
 
257
- ![image](https://github.com/user-attachments/assets/e6f0c03a-e98e-40cd-af1d-38489d650fb1)
259
+ ![image](https://github.com/user-attachments/assets/798c994f-af7e-4be6-8f8c-87157dcf94e0)
258
260
 
259
261
  ## How to install
260
262
 
@@ -0,0 +1,36 @@
1
+ restiny/__about__.py,sha256=0PaI2eSOCp5kkNcpKpUSbLHf66rL9xQzFpYyLGpEtyM,22
2
+ restiny/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ restiny/__main__.py,sha256=uUVIpbnk2nzWV5ZSEDuUUEJPKeAZW8arDl2mbekUKKU,1066
4
+ restiny/consts.py,sha256=LMwWVmOrOwZAKcnpZkBs-duMDE6QWTWD4un9y6hcVEc,11319
5
+ restiny/entities.py,sha256=hCNoy2tNQjAz6czHmTF4Ee10tqTjxNVycL1ENZry_yc,10040
6
+ restiny/enums.py,sha256=57KQPeY1YFNr15Ykr8ZglkEKdez6Djqnfzm0YaBPGfQ,996
7
+ restiny/httpx_auths.py,sha256=FqwO6W2AYNhmG5OEbQBusumH-zVvel4A8Oa7c4cbm-4,1295
8
+ restiny/utils.py,sha256=ktXrnlujBYxrPi-nDYor94r1cmJ07xyezesuZESswOc,4493
9
+ restiny/assets/__init__.py,sha256=JL1KARlToF6ZR7KeUjlDAHgwwVM2qXYaIl4wHeFW2zU,93
10
+ restiny/assets/style.tcss,sha256=LCszWlsdR_4GqwmR9C8aea_RojfCY-JyV-nA3NidF9s,925
11
+ restiny/data/db.py,sha256=aRYrjv0ecFWToj5Y0XBJ_o4QPLPY2qzRKvgUgbzG98E,1990
12
+ restiny/data/models.py,sha256=QInN6uLapn55eeWcPnq13Jkd88RKobiq69XwZjexkhM,2585
13
+ restiny/data/repos.py,sha256=ySeXrFuqmSV_Q_nIZWPBj8TY-RjKzumcId6vE1m6jZM,12105
14
+ restiny/data/sql/__init__.py,sha256=4Erfs-MC_ctZ53lXqe_FQwJDRd8SrxGrZ3_rG8o7VCU,81
15
+ restiny/ui/__init__.py,sha256=AaxD5x6SdlMMxce0hbtAWw5V2hy1zoTfy9EzcaHKMgI,374
16
+ restiny/ui/app.py,sha256=c-cLV15J9SPLcXkbmHWS11L9es5oWOoRkV3HCySqR7g,18060
17
+ restiny/ui/collections_area.py,sha256=UwK255demkwEw5uh0_XuZpjdQaR091STcV22zDZ6ukc,18501
18
+ restiny/ui/request_area.py,sha256=bxtl0g2cBgRDxMFVAu0ocdLi-a4OeQTpxOsJTkdbW_8,20795
19
+ restiny/ui/response_area.py,sha256=Rzq82muK9gXWHIHdLqetFG403n7JdE65Rka0Siw339I,4195
20
+ restiny/ui/settings_screen.py,sha256=wN--I4KY87Pe0UUaBZmDZN091jHac-7YAr8GeDbXjy8,2345
21
+ restiny/ui/url_area.py,sha256=Cc6AF9g_dRgC5TsK9ORE8As1hFq4zfG_rWp77NrrdJg,3779
22
+ restiny/widgets/__init__.py,sha256=RaU2JkRWAoJULG3rpPtMNUarPU6Yu6gJwzAsSad2hgg,895
23
+ restiny/widgets/collections_tree.py,sha256=X-wm_bkUHK9E9XDGjJE-bjeQWEqwfNyZNFTA25nDQe4,2038
24
+ restiny/widgets/confirm_prompt.py,sha256=1xdCaJZUDzV4-fQ1ztbe7uX9hJ6ZZUBghtwgReBHz9w,2147
25
+ restiny/widgets/custom_directory_tree.py,sha256=sNTaI0DBAO56MyOy6qMZPgWXiTUQbBrJdn1GtOdxrDc,1268
26
+ restiny/widgets/custom_input.py,sha256=W6gE9jbTl_R1uLSA5Dz9eBX7aNID2-rYZP3j2oNi4SA,466
27
+ restiny/widgets/custom_text_area.py,sha256=ykmG-6MiMhz6BqNzP8f14jUTWWKjsCOIEhgciP-01Y8,14032
28
+ restiny/widgets/dynamic_fields.py,sha256=S2epm-_QOsHEGhVFwDlOvIqOQkUgpGnh6pK3_JoTQ1g,16104
29
+ restiny/widgets/password_input.py,sha256=xXOfiStcUCbP_cnrS2Rz0-GmsvmOsen4G41zOpmjLD8,4057
30
+ restiny/widgets/path_chooser.py,sha256=FdG9fdgY2qD8o-7aBn8F005f8H91kbYtyF99RRGLRas,9273
31
+ restiny-0.5.0.dist-info/licenses/LICENSE,sha256=Z190MKguypkrjaCldiorEbMmBQp7ylvx09oyE4oDCTs,11361
32
+ restiny-0.5.0.dist-info/METADATA,sha256=_W4JXRugmt8LSFdTuwp46BF5T-xlI9goHEJSVwf0OsU,16137
33
+ restiny-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ restiny-0.5.0.dist-info/entry_points.txt,sha256=F9zW8bAPAwIihltqjzYow4ahmH_B6VkAHzQFA-8QOn4,50
35
+ restiny-0.5.0.dist-info/top_level.txt,sha256=1MQ_Q-fV1Dwbu4zU3g1Eg-CfRgC412X-mvMIrEdrlbk,8
36
+ restiny-0.5.0.dist-info/RECORD,,
restiny/core/__init__.py DELETED
@@ -1,15 +0,0 @@
1
- """
2
- This module contains the specific sections of the DataFox user interface (UI).
3
- """
4
-
5
- from restiny.core.request_area import RequestArea, RequestAreaData
6
- from restiny.core.response_area import ResponseArea
7
- from restiny.core.url_area import URLArea, URLAreaData
8
-
9
- __all__ = [
10
- 'RequestArea',
11
- 'RequestAreaData',
12
- 'ResponseArea',
13
- 'URLArea',
14
- 'URLAreaData',
15
- ]
restiny/core/app.py DELETED
@@ -1,348 +0,0 @@
1
- import asyncio
2
- import json
3
- import mimetypes
4
- from http import HTTPStatus
5
- from pathlib import Path
6
-
7
- import httpx
8
- import pyperclip
9
- from textual import on
10
- from textual.app import App, ComposeResult
11
- from textual.binding import Binding
12
- from textual.containers import Horizontal, Vertical
13
- from textual.events import DescendantFocus
14
- from textual.widget import Widget
15
- from textual.widgets import Footer, Header
16
-
17
- from restiny.__about__ import __version__
18
- from restiny.assets import STYLE_TCSS
19
- from restiny.core import (
20
- RequestArea,
21
- RequestAreaData,
22
- ResponseArea,
23
- URLArea,
24
- URLAreaData,
25
- )
26
- from restiny.core.response_area import ResponseAreaData
27
- from restiny.enums import BodyMode, BodyRawLanguage, ContentType
28
- from restiny.utils import build_curl_cmd
29
-
30
-
31
- class RESTinyApp(App, inherit_bindings=False):
32
- TITLE = f'RESTiny v{__version__}'
33
- SUB_TITLE = 'Minimal HTTP client, no bullshit'
34
- ENABLE_COMMAND_PALETTE = False
35
- CSS_PATH = STYLE_TCSS
36
- BINDINGS = [
37
- Binding(
38
- key='escape', action='quit', description='Quit the app', show=True
39
- ),
40
- Binding(
41
- key='f10',
42
- action='maximize_or_minimize_area',
43
- description='Maximize/Minimize area',
44
- show=True,
45
- ),
46
- Binding(
47
- key='f9',
48
- action='copy_as_curl',
49
- description='Copy as curl',
50
- show=True,
51
- ),
52
- ]
53
- theme = 'textual-dark'
54
-
55
- def __init__(self, *args, **kwargs) -> None:
56
- super().__init__(*args, **kwargs)
57
- self.current_request: asyncio.Task | None = None
58
- self.last_focused_widget: Widget | None = None
59
- self.last_focused_maximizable_area: Widget | None = None
60
-
61
- def compose(self) -> ComposeResult:
62
- yield Header(show_clock=True)
63
- with Vertical(id='main-content'):
64
- with Horizontal(classes='h-auto'):
65
- yield URLArea()
66
- with Horizontal(classes='h-1fr'):
67
- with Vertical():
68
- yield RequestArea()
69
- with Vertical():
70
- yield ResponseArea()
71
- yield Footer()
72
-
73
- def on_mount(self) -> None:
74
- self.url_area = self.query_one(URLArea)
75
- self.request_area = self.query_one(RequestArea)
76
- self.response_area = self.query_one(ResponseArea)
77
-
78
- def action_maximize_or_minimize_area(self) -> None:
79
- if self.screen.maximized:
80
- self.screen.minimize()
81
- else:
82
- self.screen.maximize(self.last_focused_maximizable_area)
83
-
84
- def action_copy_as_curl(self) -> None:
85
- url_area_data = self.url_area.get_data()
86
- request_area_data = self.request_area.get_data()
87
-
88
- method = url_area_data.method
89
- url = url_area_data.url
90
-
91
- headers = {}
92
- for header in request_area_data.headers:
93
- if not header.enabled:
94
- continue
95
-
96
- headers[header.key] = header.value
97
-
98
- params = {}
99
- for param in request_area_data.query_params:
100
- if not param.enabled:
101
- continue
102
-
103
- params[param.key] = param.value
104
-
105
- raw_body = None
106
- form_urlencoded = {}
107
- form_multipart = {}
108
- files = None
109
- if request_area_data.body.type == BodyMode.RAW:
110
- raw_body = request_area_data.body.payload
111
- elif request_area_data.body.type == BodyMode.FORM_URLENCODED:
112
- form_urlencoded = {
113
- form_field.key: form_field.value
114
- for form_field in request_area_data.body.payload
115
- if form_field.enabled
116
- }
117
- elif request_area_data.body.type == BodyMode.FORM_MULTIPART:
118
- form_multipart = {
119
- form_field.key: form_field.value
120
- for form_field in request_area_data.body.payload
121
- if form_field.enabled
122
- }
123
- elif request_area_data.body.type == BodyMode.FILE:
124
- files = [request_area_data.body.payload]
125
-
126
- curl_cmd = build_curl_cmd(
127
- method=method,
128
- url=url,
129
- headers=headers,
130
- params=params,
131
- raw_body=raw_body,
132
- form_urlencoded=form_urlencoded,
133
- form_multipart=form_multipart,
134
- files=files,
135
- )
136
- self.copy_to_clipboard(curl_cmd)
137
- self.notify(
138
- 'Command CURL copied to clipboard',
139
- severity='information',
140
- )
141
-
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:
171
- while widget is not None:
172
- if (
173
- isinstance(widget, URLArea)
174
- or isinstance(widget, RequestArea)
175
- or isinstance(widget, ResponseArea)
176
- ):
177
- return widget
178
- widget = widget.parent
179
-
180
- async def _send_request(self) -> None:
181
- url_area_data = self.url_area.get_data()
182
- request_area_data = self.request_area.get_data()
183
-
184
- self.response_area.set_data(data=None)
185
- self.response_area.loading = True
186
- self.url_area.request_pending = True
187
- try:
188
- async with httpx.AsyncClient(
189
- timeout=request_area_data.options.timeout,
190
- follow_redirects=request_area_data.options.follow_redirects,
191
- verify=request_area_data.options.verify_ssl,
192
- ) as http_client:
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
- )
198
- response = await http_client.send(request=request)
199
- self._display_response(response=response)
200
- self.response_area.is_showing_response = True
201
- except httpx.RequestError as error:
202
- error_name = type(error).__name__
203
- error_message = str(error)
204
- if error_message:
205
- self.notify(f'{error_name}: {error_message}', severity='error')
206
- else:
207
- self.notify(f'{error_name}', severity='error')
208
- self.response_area.set_data(data=None)
209
- self.response_area.is_showing_response = False
210
- except asyncio.CancelledError:
211
- self.response_area.set_data(data=None)
212
- self.response_area.is_showing_response = False
213
- finally:
214
- self.response_area.loading = False
215
- self.url_area.request_pending = False
216
-
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,
240
- )
241
-
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
- )
253
-
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'
274
- )
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,
318
- )
319
-
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
- )