restiny 0.2.0__py3-none-any.whl → 0.3.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.

Potentially problematic release.


This version of restiny might be problematic. Click here for more details.

@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from pathlib import Path
3
+ from typing import Literal
3
4
 
4
5
  from textual import on
5
6
  from textual.app import ComposeResult
@@ -15,10 +16,11 @@ from textual.widgets import (
15
16
  TabPane,
16
17
  )
17
18
 
18
- from restiny.enums import BodyMode, BodyRawLanguage
19
+ from restiny.enums import AuthMode, BodyMode, BodyRawLanguage
19
20
  from restiny.widgets import (
20
21
  CustomTextArea,
21
22
  DynamicFields,
23
+ PasswordInput,
22
24
  PathChooser,
23
25
  TextDynamicField,
24
26
  )
@@ -33,7 +35,7 @@ class HeaderField:
33
35
 
34
36
 
35
37
  @dataclass
36
- class QueryParamField:
38
+ class ParamField:
37
39
  enabled: bool
38
40
  key: str
39
41
  value: str
@@ -54,28 +56,64 @@ class FormMultipartField:
54
56
 
55
57
 
56
58
  @dataclass
57
- class RequestAreaData:
58
- @dataclass
59
- class Options:
60
- timeout: int | float | None
61
- follow_redirects: bool
62
- verify_ssl: bool
63
-
64
- @dataclass
65
- class Body:
66
- enabled: bool
67
- raw_language: BodyRawLanguage | None
68
- type: BodyMode
69
- payload: (
70
- str
71
- | Path
72
- | list[FormUrlEncodedField]
73
- | list[FormMultipartField]
74
- | None
75
- )
59
+ class BasicAuth:
60
+ username: str
61
+ password: str
62
+
63
+
64
+ @dataclass
65
+ class BearerAuth:
66
+ token: str
67
+
68
+
69
+ @dataclass
70
+ class APIKeyAuth:
71
+ key: str
72
+ value: str
73
+ where: Literal['header', 'param']
74
+
75
+
76
+ @dataclass
77
+ class DigestAuth:
78
+ username: str
79
+ password: str
80
+
81
+
82
+ @dataclass
83
+ class Options:
84
+ timeout: int | float | None
85
+ follow_redirects: bool
86
+ verify_ssl: bool
87
+
88
+
89
+ @dataclass
90
+ class Body:
91
+ enabled: bool
92
+ raw_language: BodyRawLanguage | None
93
+ mode: BodyMode
94
+ payload: (
95
+ str
96
+ | Path
97
+ | list[FormUrlEncodedField]
98
+ | list[FormMultipartField]
99
+ | None
100
+ )
101
+
102
+
103
+ _AuthType = BasicAuth | BearerAuth | APIKeyAuth | DigestAuth
104
+
76
105
 
106
+ @dataclass
107
+ class Auth:
108
+ enabled: bool
109
+ value: _AuthType
110
+
111
+
112
+ @dataclass
113
+ class RequestAreaData:
77
114
  headers: list[HeaderField]
78
- query_params: list[QueryParamField]
115
+ params: list[ParamField]
116
+ auth: Auth
79
117
  body: Body
80
118
  options: Options
81
119
 
@@ -101,11 +139,80 @@ class RequestArea(Static):
101
139
  fields=[TextDynamicField(enabled=False, key='', value='')],
102
140
  id='headers',
103
141
  )
104
- with TabPane('Query params'):
142
+ with TabPane('Params'):
105
143
  yield DynamicFields(
106
144
  fields=[TextDynamicField(enabled=False, key='', value='')],
107
145
  id='params',
108
146
  )
147
+ with TabPane('Auth'):
148
+ with Horizontal(classes='h-auto'):
149
+ yield Switch(tooltip='Enabled', id='auth-enabled')
150
+ yield Select(
151
+ (
152
+ ('Basic', AuthMode.BASIC),
153
+ ('Bearer', AuthMode.BEARER),
154
+ ('API Key', AuthMode.API_KEY),
155
+ ('Digest', AuthMode.DIGEST),
156
+ ),
157
+ allow_blank=False,
158
+ tooltip='Auth mode',
159
+ id='auth-mode',
160
+ )
161
+ with ContentSwitcher(
162
+ initial='auth-basic', id='auth-mode-switcher'
163
+ ):
164
+ with Horizontal(id='auth-basic', classes='mt-1'):
165
+ yield Input(
166
+ placeholder='Username',
167
+ select_on_focus=False,
168
+ classes='w-1fr',
169
+ id='auth-basic-username',
170
+ )
171
+ yield PasswordInput(
172
+ placeholder='Password',
173
+ select_on_focus=False,
174
+ classes='w-2fr',
175
+ id='auth-basic-password',
176
+ )
177
+ with Horizontal(id='auth-bearer', classes='mt-1'):
178
+ yield PasswordInput(
179
+ placeholder='Token',
180
+ select_on_focus=False,
181
+ id='auth-bearer-token',
182
+ )
183
+ with Horizontal(id='auth-api-key', classes='mt-1'):
184
+ yield Select(
185
+ (('Header', 'header'), ('Param', 'param')),
186
+ allow_blank=False,
187
+ tooltip='Where',
188
+ classes='w-1fr',
189
+ id='auth-api-key-where',
190
+ )
191
+ yield Input(
192
+ placeholder='Key',
193
+ classes='w-2fr',
194
+ id='auth-api-key-key',
195
+ )
196
+ yield PasswordInput(
197
+ placeholder='Value',
198
+ classes='w-3fr',
199
+ id='auth-api-key-value',
200
+ )
201
+
202
+ with Horizontal(id='auth-digest', classes='mt-1'):
203
+ yield Input(
204
+ placeholder='Username',
205
+ select_on_focus=False,
206
+ classes='w-1fr',
207
+ id='auth-digest-username',
208
+ )
209
+ yield PasswordInput(
210
+ placeholder='Password',
211
+ select_on_focus=False,
212
+ classes='w-2fr',
213
+ id='auth-digest-password',
214
+ )
215
+
109
216
  with TabPane('Body'):
110
217
  with Horizontal(classes='h-auto'):
111
218
  yield Switch(id='body-enabled', tooltip='Send body?')
@@ -117,7 +224,7 @@ class RequestArea(Static):
117
224
  ('Form (multipart)', BodyMode.FORM_MULTIPART),
118
225
  ),
119
226
  allow_blank=False,
120
- tooltip='Body type',
227
+ tooltip='Body mode',
121
228
  id='body-mode',
122
229
  )
123
230
  with ContentSwitcher(
@@ -174,8 +281,11 @@ class RequestArea(Static):
174
281
  yield Input(
175
282
  '5.5',
176
283
  placeholder='5.5',
177
- id='options-timeout',
284
+ select_on_focus=False,
285
+ type='number',
286
+ valid_empty=True,
178
287
  classes='w-1fr',
288
+ id='options-timeout',
179
289
  )
180
290
  with Horizontal(classes='mt-1 h-auto'):
181
291
  yield Switch(id='options-follow-redirects')
@@ -189,6 +299,36 @@ class RequestArea(Static):
189
299
 
190
300
  self.param_fields = self.query_one('#params', DynamicFields)
191
301
 
302
+ self.auth_enabled_switch = self.query_one('#auth-enabled', Switch)
303
+ self.auth_mode_switcher = self.query_one(
304
+ '#auth-mode-switcher', ContentSwitcher
305
+ )
306
+ self.auth_mode_select = self.query_one('#auth-mode', Select)
307
+ self.auth_basic_username_input = self.query_one(
308
+ '#auth-basic-username', Input
309
+ )
310
+ self.auth_basic_password_input = self.query_one(
311
+ '#auth-basic-password', PasswordInput
312
+ )
313
+ self.auth_bearer_token_input = self.query_one(
314
+ '#auth-bearer-token', PasswordInput
315
+ )
316
+ self.auth_api_key_key_input = self.query_one(
317
+ '#auth-api-key-key', Input
318
+ )
319
+ self.auth_api_key_value_input = self.query_one(
320
+ '#auth-api-key-value', PasswordInput
321
+ )
322
+ self.auth_api_key_where_select = self.query_one(
323
+ '#auth-api-key-where', Select
324
+ )
325
+ self.auth_digest_username_input = self.query_one(
326
+ '#auth-digest-username', Input
327
+ )
328
+ self.auth_digest_password_input = self.query_one(
329
+ '#auth-digest-password', PasswordInput
330
+ )
331
+
192
332
  self.body_enabled_switch = self.query_one('#body-enabled', Switch)
193
333
  self.body_mode_select = self.query_one('#body-mode', Select)
194
334
  self.body_mode_switcher = self.query_one(
@@ -214,8 +354,28 @@ class RequestArea(Static):
214
354
  '#options-verify-ssl', Switch
215
355
  )
216
356
 
357
+ def get_data(self) -> RequestAreaData:
358
+ return RequestAreaData(
359
+ headers=self._get_headers(),
360
+ params=self._get_params(),
361
+ auth=self._get_auth(),
362
+ body=self._get_body(),
363
+ options=self._get_options(),
364
+ )
365
+
366
+ @on(Select.Changed, '#auth-mode')
367
+ def _on_change_auth_mode(self, message: Select.Changed) -> None:
368
+ if message.value == 'basic':
369
+ self.auth_mode_switcher.current = 'auth-basic'
370
+ elif message.value == 'bearer':
371
+ self.auth_mode_switcher.current = 'auth-bearer'
372
+ elif message.value == 'api_key':
373
+ self.auth_mode_switcher.current = 'auth-api-key'
374
+ elif message.value == 'digest':
375
+ self.auth_mode_switcher.current = 'auth-digest'
376
+
217
377
  @on(Select.Changed, '#body-mode')
218
- def on_change_body_type(self, message: Select.Changed) -> None:
378
+ def _on_change_body_mode(self, message: Select.Changed) -> None:
219
379
  if message.value == BodyMode.FILE:
220
380
  self.body_mode_switcher.current = 'body-mode-file'
221
381
  elif message.value == BodyMode.RAW:
@@ -226,113 +386,122 @@ class RequestArea(Static):
226
386
  self.body_mode_switcher.current = 'body-mode-form-multipart'
227
387
 
228
388
  @on(Select.Changed, '#body-raw-language')
229
- def on_change_body_text_language(self, message: Select.Changed) -> None:
389
+ def _on_change_body_raw_language(self, message: Select.Changed) -> None:
230
390
  self.body_raw_editor.language = message.value
231
391
 
232
392
  @on(DynamicFields.FieldFilled, '#body-form-urlencoded')
233
- def on_form_filled(self, message: DynamicFields.FieldFilled) -> None:
393
+ def _on_form_filled(self, message: DynamicFields.FieldFilled) -> None:
234
394
  self.body_enabled_switch.value = True
235
395
 
236
396
  @on(DynamicFields.FieldEmpty, '#body-form-urlencoded')
237
- def on_form_empty(self, message: DynamicFields.FieldEmpty) -> None:
397
+ def _on_form_empty(self, message: DynamicFields.FieldEmpty) -> None:
238
398
  if not message.control.filled_fields:
239
399
  self.body_enabled_switch.value = False
240
400
 
241
401
  @on(CustomTextArea.Changed, '#body-raw')
242
- def on_change_body_text(self, message: CustomTextArea.Changed) -> None:
402
+ def _on_change_body_raw(self, message: CustomTextArea.Changed) -> None:
243
403
  if self.body_raw_editor.text == '':
244
404
  self.body_enabled_switch.value = False
245
405
  else:
246
406
  self.body_enabled_switch.value = True
247
407
 
248
- @on(Input.Changed, '#options-timeout')
249
- def on_change_timeout(self, message: Input.Changed) -> None:
250
- new_value = message.value
251
-
252
- if new_value == '':
253
- return
254
-
255
- try:
256
- float(new_value)
257
- except Exception:
258
- self.options_timeout_input.value = (
259
- self.options_timeout_input.value[:-1]
408
+ def _get_headers(self) -> list[HeaderField]:
409
+ return [
410
+ HeaderField(
411
+ enabled=header_field['enabled'],
412
+ key=header_field['key'],
413
+ value=header_field['value'],
414
+ )
415
+ for header_field in self.header_fields.values
416
+ ]
417
+
418
+ def _get_params(self) -> list[ParamField]:
419
+ return [
420
+ ParamField(
421
+ enabled=param_field['enabled'],
422
+ key=param_field['key'],
423
+ value=param_field['value'],
424
+ )
425
+ for param_field in self.param_fields.values
426
+ ]
427
+
428
+ def _get_auth(self) -> _AuthType:
429
+ if self.auth_mode_select.value == AuthMode.BASIC:
430
+ return Auth(
431
+ enabled=self.auth_enabled_switch.value,
432
+ value=BasicAuth(
433
+ username=self.auth_basic_username_input.value,
434
+ password=self.auth_basic_password_input.value,
435
+ ),
436
+ )
437
+ elif self.auth_mode_select.value == AuthMode.BEARER:
438
+ return Auth(
439
+ enabled=self.auth_enabled_switch.value,
440
+ value=BearerAuth(token=self.auth_bearer_token_input.value),
441
+ )
442
+ elif self.auth_mode_select.value == AuthMode.API_KEY:
443
+ return Auth(
444
+ enabled=self.auth_enabled_switch.value,
445
+ value=APIKeyAuth(
446
+ key=self.auth_api_key_key_input.value,
447
+ value=self.auth_api_key_value_input.value,
448
+ where=self.auth_api_key_where_select.value,
449
+ ),
450
+ )
451
+ elif self.auth_mode_select.value == AuthMode.DIGEST:
452
+ return Auth(
453
+ enabled=self.auth_enabled_switch.value,
454
+ value=DigestAuth(
455
+ username=self.auth_digest_username_input.value,
456
+ password=self.auth_digest_password_input.value,
457
+ ),
260
458
  )
261
459
 
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
- )
460
+ def _get_body(self) -> Body:
461
+ body_send: bool = self.body_enabled_switch.value
462
+ body_mode: str = BodyMode(self.body_mode_select.value)
463
+
464
+ payload = None
465
+ if body_mode == BodyMode.RAW:
466
+ payload = self.body_raw_editor.text
467
+ elif body_mode == BodyMode.FILE:
468
+ payload = self.body_file_path_chooser.path
469
+ elif body_mode == BodyMode.FORM_URLENCODED:
470
+ payload = []
471
+ for form_item in self.body_form_urlencoded_fields.values:
472
+ payload.append(
473
+ FormUrlEncodedField(
474
+ enabled=form_item['enabled'],
475
+ key=form_item['key'],
476
+ value=form_item['value'],
301
477
  )
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
- )
478
+ )
479
+ elif body_mode == BodyMode.FORM_MULTIPART:
480
+ payload = []
481
+ for form_item in self.body_form_multipart_fields.values:
482
+ payload.append(
483
+ FormMultipartField(
484
+ enabled=form_item['enabled'],
485
+ key=form_item['key'],
486
+ value=form_item['value'],
311
487
  )
488
+ )
312
489
 
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
- )
490
+ return Body(
491
+ enabled=body_send,
492
+ raw_language=BodyRawLanguage(self.body_raw_language_select.value),
493
+ mode=body_mode,
494
+ payload=payload,
495
+ )
321
496
 
322
- def get_options() -> RequestAreaData.Options:
497
+ def _get_options(self) -> Options:
498
+ try:
499
+ timeout = float(self.options_timeout_input.value)
500
+ except ValueError:
323
501
  timeout = None
324
- if self.options_timeout_input.value:
325
- timeout = float(self.options_timeout_input.value)
326
502
 
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
- )
332
-
333
- return RequestAreaData(
334
- headers=get_headers(),
335
- query_params=get_query_params(),
336
- body=get_body(),
337
- options=get_options(),
503
+ return Options(
504
+ timeout=timeout,
505
+ follow_redirects=self.options_follow_redirects_switch.value,
506
+ verify_ssl=self.options_verify_ssl_switch.value,
338
507
  )
@@ -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