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.
@@ -1,4 +1,3 @@
1
- import mimetypes
2
1
  from dataclasses import dataclass
3
2
  from pathlib import Path
4
3
 
@@ -19,10 +18,11 @@ from textual.widgets import (
19
18
  from restiny.enums import BodyMode, BodyRawLanguage
20
19
  from restiny.widgets import (
21
20
  CustomTextArea,
22
- DynamicField,
23
21
  DynamicFields,
24
22
  PathChooser,
23
+ TextDynamicField,
25
24
  )
25
+ from restiny.widgets.dynamic_fields import TextOrFileDynamicField
26
26
 
27
27
 
28
28
  @dataclass
@@ -98,12 +98,12 @@ class RequestArea(Static):
98
98
  with TabbedContent():
99
99
  with TabPane('Headers'):
100
100
  yield DynamicFields(
101
- fields=[DynamicField(enabled=False, key='', value='')],
101
+ fields=[TextDynamicField(enabled=False, key='', value='')],
102
102
  id='headers',
103
103
  )
104
104
  with TabPane('Query params'):
105
105
  yield DynamicFields(
106
- fields=[DynamicField(enabled=False, key='', value='')],
106
+ fields=[TextDynamicField(enabled=False, key='', value='')],
107
107
  id='params',
108
108
  )
109
109
  with TabPane('Body'):
@@ -114,7 +114,7 @@ class RequestArea(Static):
114
114
  ('Raw', BodyMode.RAW),
115
115
  ('File', BodyMode.FILE),
116
116
  ('Form (urlencoded)', BodyMode.FORM_URLENCODED),
117
- # ('Form (multipart)', BodyMode.FORM_MULTIPART)
117
+ ('Form (multipart)', BodyMode.FORM_MULTIPART),
118
118
  ),
119
119
  allow_blank=False,
120
120
  tooltip='Body type',
@@ -149,9 +149,24 @@ class RequestArea(Static):
149
149
  id='body-mode-form-urlencoded', classes='h-auto mt-1'
150
150
  ):
151
151
  yield DynamicFields(
152
- [DynamicField(enabled=False, key='', value='')],
152
+ [
153
+ TextDynamicField(
154
+ enabled=False, key='', value=''
155
+ )
156
+ ],
153
157
  id='body-form-urlencoded',
154
158
  )
159
+ with Horizontal(
160
+ id='body-mode-form-multipart', classes='h-auto mt-1'
161
+ ):
162
+ yield DynamicFields(
163
+ [
164
+ TextOrFileDynamicField(
165
+ enabled=False, key='', value=''
166
+ )
167
+ ],
168
+ id='body-form-multipart',
169
+ )
155
170
 
156
171
  with TabPane('Options'):
157
172
  with Horizontal(classes='h-auto'):
@@ -159,6 +174,7 @@ class RequestArea(Static):
159
174
  yield Input(
160
175
  '5.5',
161
176
  placeholder='5.5',
177
+ select_on_focus=False,
162
178
  id='options-timeout',
163
179
  classes='w-1fr',
164
180
  )
@@ -187,6 +203,9 @@ class RequestArea(Static):
187
203
  self.body_form_urlencoded_fields = self.query_one(
188
204
  '#body-form-urlencoded', DynamicFields
189
205
  )
206
+ self.body_form_multipart_fields = self.query_one(
207
+ '#body-form-multipart', DynamicFields
208
+ )
190
209
 
191
210
  self.options_timeout_input = self.query_one('#options-timeout', Input)
192
211
  self.options_follow_redirects_switch = self.query_one(
@@ -196,61 +215,47 @@ class RequestArea(Static):
196
215
  '#options-verify-ssl', Switch
197
216
  )
198
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
+
199
226
  @on(Select.Changed, '#body-mode')
200
- def on_change_body_type(self, message: Select.Changed) -> None:
227
+ def _on_change_body_type(self, message: Select.Changed) -> None:
201
228
  if message.value == BodyMode.FILE:
202
229
  self.body_mode_switcher.current = 'body-mode-file'
203
230
  elif message.value == BodyMode.RAW:
204
231
  self.body_mode_switcher.current = 'body-mode-raw'
205
232
  elif message.value == BodyMode.FORM_URLENCODED:
206
233
  self.body_mode_switcher.current = 'body-mode-form-urlencoded'
234
+ elif message.value == BodyMode.FORM_MULTIPART:
235
+ self.body_mode_switcher.current = 'body-mode-form-multipart'
207
236
 
208
237
  @on(Select.Changed, '#body-raw-language')
209
- def on_change_body_text_language(self, message: Select.Changed) -> None:
238
+ def _on_change_body_raw_language(self, message: Select.Changed) -> None:
210
239
  self.body_raw_editor.language = message.value
211
240
 
212
241
  @on(DynamicFields.FieldFilled, '#body-form-urlencoded')
213
- def on_form_filled(self, message: DynamicFields.FieldFilled) -> None:
242
+ def _on_form_filled(self, message: DynamicFields.FieldFilled) -> None:
214
243
  self.body_enabled_switch.value = True
215
244
 
216
245
  @on(DynamicFields.FieldEmpty, '#body-form-urlencoded')
217
- def on_form_empty(self, message: DynamicFields.FieldEmpty) -> None:
246
+ def _on_form_empty(self, message: DynamicFields.FieldEmpty) -> None:
218
247
  if not message.control.filled_fields:
219
248
  self.body_enabled_switch.value = False
220
249
 
221
250
  @on(CustomTextArea.Changed, '#body-raw')
222
- def on_change_body_text(self, message: CustomTextArea.Changed) -> None:
251
+ def _on_change_body_raw(self, message: CustomTextArea.Changed) -> None:
223
252
  if self.body_raw_editor.text == '':
224
253
  self.body_enabled_switch.value = False
225
254
  else:
226
255
  self.body_enabled_switch.value = True
227
256
 
228
- @on(PathChooser.Changed)
229
- async def on_change_file(self, message: PathChooser.Changed) -> None:
230
- content_type_header_field: DynamicField | None = None
231
- for header_field in self.header_fields.fields:
232
- if header_field.key.lower() == 'content-type':
233
- content_type_header_field = header_field
234
- break
235
-
236
- content_type: str | None = mimetypes.guess_type(str(message.path))[0]
237
- if not content_type:
238
- return
239
-
240
- if content_type_header_field:
241
- content_type_header_field.value = content_type
242
- return
243
-
244
- empty_field = self.header_fields.empty_fields[0]
245
- empty_field.enabled = True
246
- empty_field.key = 'Content-Type'
247
- empty_field.value = content_type
248
- await self.header_fields.add_field(
249
- field=DynamicField(enabled=False, key='', value='')
250
- )
251
-
252
257
  @on(Input.Changed, '#options-timeout')
253
- def on_change_timeout(self, message: Input.Changed) -> None:
258
+ def _on_change_timeout(self, message: Input.Changed) -> None:
254
259
  new_value = message.value
255
260
 
256
261
  if new_value == '':
@@ -263,73 +268,70 @@ class RequestArea(Static):
263
268
  self.options_timeout_input.value[:-1]
264
269
  )
265
270
 
266
- def get_data(self) -> RequestAreaData:
267
- def get_headers() -> list[HeaderField]:
268
- return [
269
- HeaderField(
270
- enabled=header_field['enabled'],
271
- key=header_field['key'],
272
- value=header_field['value'],
273
- )
274
- for header_field in self.header_fields.values
275
- ]
276
-
277
- def get_query_params() -> list[QueryParamField]:
278
- return [
279
- QueryParamField(
280
- enabled=query_param_field['enabled'],
281
- key=query_param_field['key'],
282
- value=query_param_field['value'],
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'],
308
+ )
283
309
  )
284
- for query_param_field in self.param_fields.values
285
- ]
286
-
287
- def get_body() -> RequestAreaData.Body:
288
- body_send: bool = self.body_enabled_switch.value
289
- body_type: str = BodyMode(self.body_mode_select.value)
290
-
291
- payload = None
292
- if body_type == BodyMode.RAW:
293
- payload = self.body_raw_editor.text
294
- elif body_type == BodyMode.FILE:
295
- payload = self.body_file_path_chooser.path
296
- elif body_type == BodyMode.FORM_URLENCODED:
297
- payload = []
298
- for form_item in self.body_form_urlencoded_fields.values:
299
- payload.append(
300
- FormUrlEncodedField(
301
- enabled=form_item['enabled'],
302
- key=form_item['key'],
303
- value=form_item['value'],
304
- )
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'],
305
318
  )
319
+ )
306
320
 
307
- return RequestAreaData.Body(
308
- enabled=body_send,
309
- raw_language=BodyRawLanguage(
310
- self.body_raw_language_select.value
311
- ),
312
- type=body_type,
313
- payload=payload,
314
- )
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
+ )
315
327
 
316
- def get_options() -> RequestAreaData.Options:
317
- timeout = None
318
- if self.options_timeout_input.value:
319
- timeout = float(self.options_timeout_input.value)
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)
320
332
 
321
- return RequestAreaData.Options(
322
- timeout=timeout,
323
- follow_redirects=self.options_follow_redirects_switch.value,
324
- verify_ssl=self.options_verify_ssl_switch.value,
325
- )
326
-
327
- return RequestAreaData(
328
- headers=get_headers(),
329
- query_params=get_query_params(),
330
- body=get_body(),
331
- 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,
332
337
  )
333
-
334
- def set_content_type() -> None:
335
- raise NotImplementedError()
@@ -1,6 +1,9 @@
1
+ from dataclasses import dataclass
2
+ from http import HTTPStatus
3
+
1
4
  from textual import on
2
5
  from textual.app import ComposeResult
3
- from textual.reactive import Reactive
6
+ from textual.containers import VerticalScroll
4
7
  from textual.widgets import (
5
8
  ContentSwitcher,
6
9
  DataTable,
@@ -15,6 +18,16 @@ from restiny.enums import BodyRawLanguage
15
18
  from restiny.widgets import CustomTextArea
16
19
 
17
20
 
21
+ @dataclass
22
+ class ResponseAreaData:
23
+ status: HTTPStatus
24
+ size: int
25
+ elapsed_time: float | int
26
+ headers: dict
27
+ body_raw_language: BodyRawLanguage
28
+ body_raw: str
29
+
30
+
18
31
  # TODO: Implement 'Trace' tab pane
19
32
  class ResponseArea(Static):
20
33
  ALLOW_MAXIMIZE = True
@@ -38,8 +51,6 @@ class ResponseArea(Static):
38
51
 
39
52
  """
40
53
 
41
- has_response: bool = Reactive(False, layout=True, init=True)
42
-
43
54
  def compose(self) -> ComposeResult:
44
55
  with ContentSwitcher(id='response-switcher', initial='no-content'):
45
56
  yield Label(
@@ -49,7 +60,8 @@ class ResponseArea(Static):
49
60
 
50
61
  with TabbedContent(id='content'):
51
62
  with TabPane('Headers'):
52
- yield DataTable(show_cursor=False, id='headers')
63
+ with VerticalScroll():
64
+ yield DataTable(show_cursor=False, id='headers')
53
65
  with TabPane('Body'):
54
66
  yield Select(
55
67
  (
@@ -60,11 +72,11 @@ class ResponseArea(Static):
60
72
  ('XML', BodyRawLanguage.XML),
61
73
  ),
62
74
  allow_blank=False,
63
- tooltip='Body type',
64
- id='body-type',
75
+ tooltip='Syntax highlighting for the response body',
76
+ id='body-raw-language',
65
77
  )
66
78
  yield CustomTextArea.code_editor(
67
- id='body', read_only=True, classes='mt-1'
79
+ id='body-raw', read_only=True, classes='mt-1'
68
80
  )
69
81
 
70
82
  def on_mount(self) -> None:
@@ -73,25 +85,46 @@ class ResponseArea(Static):
73
85
  )
74
86
 
75
87
  self.headers_data_table = self.query_one('#headers', DataTable)
76
- self.body_type_select = self.query_one('#body-type', Select)
77
- self.body_text_area = self.query_one('#body', CustomTextArea)
88
+ self.body_raw_language_select = self.query_one(
89
+ '#body-raw-language', Select
90
+ )
91
+ self.body_raw_editor = self.query_one('#body-raw', CustomTextArea)
78
92
 
79
93
  self.headers_data_table.add_columns('Key', 'Value')
80
94
 
81
- @on(Select.Changed, '#body-type')
82
- def on_body_type_changed(self, message: Select.Changed) -> None:
83
- self.body_text_area.language = self.body_type_select.value
95
+ def set_data(self, data: ResponseAreaData | None) -> None:
96
+ self.border_title = self.BORDER_TITLE
97
+ self.border_subtitle = ''
98
+ self.headers_data_table.clear()
99
+ self.body_raw_language_select.value = BodyRawLanguage.PLAIN
100
+ self.body_raw_editor.clear()
84
101
 
85
- def watch_has_response(self, value: bool) -> None:
102
+ if data is None:
103
+ return
104
+
105
+ self.border_title = f'Response - {data.status} {data.status.phrase}'
106
+ self.border_subtitle = (
107
+ f'{data.size} bytes in {data.elapsed_time} seconds'
108
+ )
109
+ for header_key, header_value in data.headers.items():
110
+ self.headers_data_table.add_row(header_key, header_value)
111
+ self.body_raw_language_select.value = data.body_raw_language
112
+ self.body_raw_editor.text = data.body_raw
113
+
114
+ @property
115
+ def is_showing_response(self) -> bool:
116
+ if self._response_switcher.current == 'content':
117
+ return True
118
+ elif self._response_switcher.current == 'no-content':
119
+ return False
120
+
121
+ @is_showing_response.setter
122
+ def is_showing_response(self, value: bool) -> None:
86
123
  if value is True:
87
124
  self._response_switcher.current = 'content'
88
125
  elif value is False:
89
126
  self._response_switcher.current = 'no-content'
90
- self.reset_response()
91
127
 
92
- def reset_response(self) -> None:
93
- self.border_title = self.BORDER_TITLE
94
- self.border_subtitle = ''
95
- self.headers_data_table.clear()
96
- self.body_type_select.value = BodyRawLanguage.PLAIN
97
- self.body_text_area.clear()
128
+ @on(Select.Changed, '#body-raw-language')
129
+ def _on_body_raw_language_changed(self, message: Select.Changed) -> None:
130
+ self.body_raw_editor.language = self.body_raw_language_select.value
restiny/core/url_area.py CHANGED
@@ -3,7 +3,6 @@ from dataclasses import dataclass
3
3
  from textual import on
4
4
  from textual.app import ComposeResult
5
5
  from textual.message import Message
6
- from textual.reactive import Reactive
7
6
  from textual.widgets import Button, ContentSwitcher, Input, Select, Static
8
7
 
9
8
  from restiny.enums import HTTPMethod
@@ -29,8 +28,6 @@ class URLArea(Static):
29
28
  }
30
29
  """
31
30
 
32
- request_pending = Reactive(False)
33
-
34
31
  class SendRequest(Message):
35
32
  """
36
33
  Sent when the user send a request.
@@ -47,11 +44,15 @@ class URLArea(Static):
47
44
  def __init__(self) -> None:
48
45
  super().__init__()
49
46
 
47
+ def __init__(self, *args, **kwargs) -> None:
48
+ super().__init__(*args, **kwargs)
49
+ self._request_pending = False
50
+
50
51
  def compose(self) -> ComposeResult:
51
52
  yield Select.from_values(
52
53
  values=HTTPMethod.values(), allow_blank=False, id='method'
53
54
  )
54
- yield Input(placeholder='Enter URL', id='url')
55
+ yield Input(placeholder='Enter URL', select_on_focus=False, id='url')
55
56
  with ContentSwitcher(
56
57
  id='request-button-switcher', initial='send-request'
57
58
  ):
@@ -78,9 +79,28 @@ class URLArea(Static):
78
79
  self.send_request_button = self.query_one('#send-request', Button)
79
80
  self.cancel_request_button = self.query_one('#cancel-request', Button)
80
81
 
82
+ def get_data(self) -> URLAreaData:
83
+ return URLAreaData(
84
+ method=self.method_select.value,
85
+ url=self.url_input.value,
86
+ )
87
+
88
+ @property
89
+ def request_pending(self) -> bool:
90
+ return self._request_pending
91
+
92
+ @request_pending.setter
93
+ def request_pending(self, value: bool) -> None:
94
+ if value is True:
95
+ self._request_button_switcher.current = 'cancel-request'
96
+ elif value is False:
97
+ self._request_button_switcher.current = 'send-request'
98
+
99
+ self._request_pending = value
100
+
81
101
  @on(Button.Pressed, '#send-request')
82
- @on(Input.Submitted)
83
- def on_send_request(
102
+ @on(Input.Submitted, '#url')
103
+ def _on_send_request(
84
104
  self, message: Button.Pressed | Input.Submitted
85
105
  ) -> None:
86
106
  if self.request_pending:
@@ -88,22 +108,10 @@ class URLArea(Static):
88
108
 
89
109
  self.post_message(message=self.SendRequest())
90
110
 
91
- @on(Input.Submitted)
92
111
  @on(Button.Pressed, '#cancel-request')
93
- def on_cancel_request(self, message: Button.Pressed) -> None:
112
+ @on(Input.Submitted, '#url')
113
+ def _on_cancel_request(self, message: Button.Pressed) -> None:
94
114
  if not self.request_pending:
95
115
  return
96
116
 
97
117
  self.post_message(message=self.CancelRequest())
98
-
99
- def watch_request_pending(self, value: bool) -> None:
100
- if value is True:
101
- self._request_button_switcher.current = 'cancel-request'
102
- elif value is False:
103
- self._request_button_switcher.current = 'send-request'
104
-
105
- def get_data(self) -> URLAreaData:
106
- return URLAreaData(
107
- method=self.method_select.value,
108
- url=self.url_input.value,
109
- )
restiny/enums.py CHANGED
@@ -39,6 +39,5 @@ class ContentType(StrEnum):
39
39
  JSON = 'application/json'
40
40
  YAML = 'application/x-yaml'
41
41
  XML = 'application/xml'
42
-
43
42
  FORM_URLENCODED = 'application/x-www-form-urlencoded'
44
43
  FORM_MULTIPART = 'multipart/form-data'
@@ -4,11 +4,11 @@ This module contains reusable widgets used in the DataFox interface.
4
4
 
5
5
  from restiny.widgets.custom_directory_tree import CustomDirectoryTree
6
6
  from restiny.widgets.custom_text_area import CustomTextArea
7
- from restiny.widgets.dynamic_fields import DynamicField, DynamicFields
7
+ from restiny.widgets.dynamic_fields import DynamicFields, TextDynamicField
8
8
  from restiny.widgets.path_chooser import PathChooser
9
9
 
10
10
  __all__ = [
11
- 'DynamicField',
11
+ 'TextDynamicField',
12
12
  'DynamicFields',
13
13
  'CustomDirectoryTree',
14
14
  'CustomTextArea',