restiny 0.2.1__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.

restiny/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.2.1'
1
+ __version__ = '0.3.0'
restiny/assets/style.tcss CHANGED
@@ -3,6 +3,10 @@ Button {
3
3
  max-width: 100%;
4
4
  }
5
5
 
6
+ Input {
7
+ width: 1fr;
8
+ }
9
+
6
10
  .hidden {
7
11
  display: none;
8
12
  }
@@ -19,6 +23,10 @@ Button {
19
23
  width: 2fr;
20
24
  }
21
25
 
26
+ .w-3fr {
27
+ width: 3fr;
28
+ }
29
+
22
30
  .h-auto {
23
31
  height: auto;
24
32
  }
@@ -28,7 +36,11 @@ Button {
28
36
  }
29
37
 
30
38
  .h-2fr {
31
- width: 2fr;
39
+ height: 2fr;
40
+ }
41
+
42
+ .h-3fr {
43
+ height: 3fr;
32
44
  }
33
45
 
34
46
  .p-0 {
restiny/core/app.py CHANGED
@@ -14,6 +14,7 @@ from textual.events import DescendantFocus
14
14
  from textual.widget import Widget
15
15
  from textual.widgets import Footer, Header
16
16
 
17
+ from restiny import httpx_auths
17
18
  from restiny.__about__ import __version__
18
19
  from restiny.assets import STYLE_TCSS
19
20
  from restiny.core import (
@@ -23,6 +24,12 @@ from restiny.core import (
23
24
  URLArea,
24
25
  URLAreaData,
25
26
  )
27
+ from restiny.core.request_area import (
28
+ APIKeyAuth,
29
+ BasicAuth,
30
+ BearerAuth,
31
+ DigestAuth,
32
+ )
26
33
  from restiny.core.response_area import ResponseAreaData
27
34
  from restiny.enums import BodyMode, BodyRawLanguage, ContentType
28
35
  from restiny.utils import build_curl_cmd
@@ -64,10 +71,8 @@ class RESTinyApp(App, inherit_bindings=False):
64
71
  with Horizontal(classes='h-auto'):
65
72
  yield URLArea()
66
73
  with Horizontal(classes='h-1fr'):
67
- with Vertical():
68
- yield RequestArea()
69
- with Vertical():
70
- yield ResponseArea()
74
+ yield RequestArea()
75
+ yield ResponseArea()
71
76
  yield Footer()
72
77
 
73
78
  def on_mount(self) -> None:
@@ -96,42 +101,78 @@ class RESTinyApp(App, inherit_bindings=False):
96
101
  headers[header.key] = header.value
97
102
 
98
103
  params = {}
99
- for param in request_area_data.query_params:
104
+ for param in request_area_data.params:
100
105
  if not param.enabled:
101
106
  continue
102
107
 
103
108
  params[param.key] = param.value
104
109
 
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]
110
+ body_raw = None
111
+ body_form_urlencoded = {}
112
+ body_form_multipart = {}
113
+ body_files = None
114
+ if request_area_data.body.enabled:
115
+ if request_area_data.body.mode == BodyMode.RAW:
116
+ body_raw = request_area_data.body.payload
117
+ elif request_area_data.body.mode == BodyMode.FORM_URLENCODED:
118
+ body_form_urlencoded = {
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.mode == BodyMode.FORM_MULTIPART:
124
+ body_form_multipart = {
125
+ form_field.key: form_field.value
126
+ for form_field in request_area_data.body.payload
127
+ if form_field.enabled
128
+ }
129
+ elif request_area_data.body.mode == BodyMode.FILE:
130
+ body_files = [request_area_data.body.payload]
131
+
132
+ auth_basic = None
133
+ auth_bearer = None
134
+ auth_api_key_header = None
135
+ auth_api_key_param = None
136
+ auth_digest = None
137
+ if request_area_data.auth.enabled:
138
+ if isinstance(request_area_data.auth.value, BasicAuth):
139
+ auth_basic = (
140
+ request_area_data.auth.value.username,
141
+ request_area_data.auth.value.password,
142
+ )
143
+ elif isinstance(request_area_data.auth.value, BearerAuth):
144
+ auth_bearer = request_area_data.auth.value.token
145
+ elif isinstance(request_area_data.auth.value, APIKeyAuth):
146
+ if request_area_data.auth.value.where == 'header':
147
+ auth_api_key_header = (
148
+ request_area_data.auth.value.key,
149
+ request_area_data.auth.value.value,
150
+ )
151
+ elif request_area_data.auth.value.where == 'param':
152
+ auth_api_key_param = (
153
+ request_area_data.auth.value.key,
154
+ request_area_data.auth.value.value,
155
+ )
156
+ elif isinstance(request_area_data.auth.value, DigestAuth):
157
+ auth_digest = (
158
+ request_area_data.auth.value.username,
159
+ request_area_data.auth.value.password,
160
+ )
125
161
 
126
162
  curl_cmd = build_curl_cmd(
127
163
  method=method,
128
164
  url=url,
129
165
  headers=headers,
130
166
  params=params,
131
- raw_body=raw_body,
132
- form_urlencoded=form_urlencoded,
133
- form_multipart=form_multipart,
134
- files=files,
167
+ body_raw=body_raw,
168
+ body_form_urlencoded=body_form_urlencoded,
169
+ body_form_multipart=body_form_multipart,
170
+ body_files=body_files,
171
+ auth_basic=auth_basic,
172
+ auth_bearer=auth_bearer,
173
+ auth_api_key_header=auth_api_key_header,
174
+ auth_api_key_param=auth_api_key_param,
175
+ auth_digest=auth_digest,
135
176
  )
136
177
  self.copy_to_clipboard(curl_cmd)
137
178
  self.notify(
@@ -195,7 +236,10 @@ class RESTinyApp(App, inherit_bindings=False):
195
236
  url_area_data=url_area_data,
196
237
  request_area_data=request_area_data,
197
238
  )
198
- response = await http_client.send(request=request)
239
+ auth = self._build_auth(
240
+ request_area_data=request_area_data,
241
+ )
242
+ response = await http_client.send(request=request, auth=auth)
199
243
  self._display_response(response=response)
200
244
  self.response_area.is_showing_response = True
201
245
  except httpx.RequestError as error:
@@ -216,18 +260,18 @@ class RESTinyApp(App, inherit_bindings=False):
216
260
 
217
261
  def _build_request(
218
262
  self,
219
- http_client: httpx.Client,
263
+ http_client: httpx.AsyncClient,
220
264
  url_area_data: URLAreaData,
221
265
  request_area_data: RequestAreaData,
222
- ) -> httpx.Request:
266
+ ) -> tuple[httpx.Request, httpx.Auth | None]:
223
267
  headers: dict[str, str] = {
224
268
  header.key: header.value
225
269
  for header in request_area_data.headers
226
270
  if header.enabled
227
271
  }
228
- query_params: dict[str, str] = {
272
+ params: dict[str, str] = {
229
273
  param.key: param.value
230
- for param in request_area_data.query_params
274
+ for param in request_area_data.params
231
275
  if param.enabled
232
276
  }
233
277
 
@@ -236,10 +280,10 @@ class RESTinyApp(App, inherit_bindings=False):
236
280
  method=url_area_data.method,
237
281
  url=url_area_data.url,
238
282
  headers=headers,
239
- params=query_params,
283
+ params=params,
240
284
  )
241
285
 
242
- if request_area_data.body.type == BodyMode.RAW:
286
+ if request_area_data.body.mode == BodyMode.RAW:
243
287
  raw_language_to_content_type = {
244
288
  BodyRawLanguage.JSON: ContentType.JSON,
245
289
  BodyRawLanguage.YAML: ContentType.YAML,
@@ -262,10 +306,10 @@ class RESTinyApp(App, inherit_bindings=False):
262
306
  method=url_area_data.method,
263
307
  url=url_area_data.url,
264
308
  headers=headers,
265
- params=query_params,
309
+ params=params,
266
310
  content=raw,
267
311
  )
268
- elif request_area_data.body.type == BodyMode.FILE:
312
+ elif request_area_data.body.mode == BodyMode.FILE:
269
313
  file = request_area_data.body.payload
270
314
  if 'content-type' not in headers:
271
315
  headers['content-type'] = (
@@ -276,10 +320,10 @@ class RESTinyApp(App, inherit_bindings=False):
276
320
  method=url_area_data.method,
277
321
  url=url_area_data.url,
278
322
  headers=headers,
279
- params=query_params,
323
+ params=params,
280
324
  content=file.read_bytes(),
281
325
  )
282
- elif request_area_data.body.type == BodyMode.FORM_URLENCODED:
326
+ elif request_area_data.body.mode == BodyMode.FORM_URLENCODED:
283
327
  form_urlencoded = {
284
328
  form_item.key: form_item.value
285
329
  for form_item in request_area_data.body.payload
@@ -289,10 +333,10 @@ class RESTinyApp(App, inherit_bindings=False):
289
333
  method=url_area_data.method,
290
334
  url=url_area_data.url,
291
335
  headers=headers,
292
- params=query_params,
336
+ params=params,
293
337
  data=form_urlencoded,
294
338
  )
295
- elif request_area_data.body.type == BodyMode.FORM_MULTIPART:
339
+ elif request_area_data.body.mode == BodyMode.FORM_MULTIPART:
296
340
  form_multipart_str = {
297
341
  form_item.key: form_item.value
298
342
  for form_item in request_area_data.body.payload
@@ -312,11 +356,42 @@ class RESTinyApp(App, inherit_bindings=False):
312
356
  method=url_area_data.method,
313
357
  url=url_area_data.url,
314
358
  headers=headers,
315
- params=query_params,
359
+ params=params,
316
360
  data=form_multipart_str,
317
361
  files=form_multipart_files,
318
362
  )
319
363
 
364
+ def _build_auth(
365
+ self,
366
+ request_area_data: RequestAreaData,
367
+ ) -> httpx.Auth | None:
368
+ auth = request_area_data.auth
369
+
370
+ if not auth.enabled:
371
+ return
372
+
373
+ if isinstance(auth.value, BasicAuth):
374
+ return httpx.BasicAuth(
375
+ username=auth.value.username,
376
+ password=auth.value.password,
377
+ )
378
+ elif isinstance(auth.value, BearerAuth):
379
+ return httpx_auths.BearerAuth(token=auth.value.token)
380
+ elif isinstance(auth.value, APIKeyAuth):
381
+ if auth.value.where == 'header':
382
+ return httpx_auths.APIKeyHeaderAuth(
383
+ key=auth.value.key, value=auth.value.value
384
+ )
385
+ elif auth.value.where == 'param':
386
+ return httpx_auths.APIKeyParamAuth(
387
+ key=auth.value.key, value=auth.value.value
388
+ )
389
+ elif isinstance(auth.value, DigestAuth):
390
+ return httpx.DigestAuth(
391
+ username=auth.value.username,
392
+ password=auth.value.password,
393
+ )
394
+
320
395
  def _display_response(self, response: httpx.Response) -> None:
321
396
  status = HTTPStatus(response.status_code)
322
397
  size = response.num_bytes_downloaded
@@ -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']
76
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
+
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(
@@ -175,8 +282,10 @@ class RequestArea(Static):
175
282
  '5.5',
176
283
  placeholder='5.5',
177
284
  select_on_focus=False,
178
- id='options-timeout',
285
+ type='number',
286
+ valid_empty=True,
179
287
  classes='w-1fr',
288
+ id='options-timeout',
180
289
  )
181
290
  with Horizontal(classes='mt-1 h-auto'):
182
291
  yield Switch(id='options-follow-redirects')
@@ -190,6 +299,36 @@ class RequestArea(Static):
190
299
 
191
300
  self.param_fields = self.query_one('#params', DynamicFields)
192
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
+
193
332
  self.body_enabled_switch = self.query_one('#body-enabled', Switch)
194
333
  self.body_mode_select = self.query_one('#body-mode', Select)
195
334
  self.body_mode_switcher = self.query_one(
@@ -218,13 +357,25 @@ class RequestArea(Static):
218
357
  def get_data(self) -> RequestAreaData:
219
358
  return RequestAreaData(
220
359
  headers=self._get_headers(),
221
- query_params=self._get_query_params(),
360
+ params=self._get_params(),
361
+ auth=self._get_auth(),
222
362
  body=self._get_body(),
223
363
  options=self._get_options(),
224
364
  )
225
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
+
226
377
  @on(Select.Changed, '#body-mode')
227
- def _on_change_body_type(self, message: Select.Changed) -> None:
378
+ def _on_change_body_mode(self, message: Select.Changed) -> None:
228
379
  if message.value == BodyMode.FILE:
229
380
  self.body_mode_switcher.current = 'body-mode-file'
230
381
  elif message.value == BodyMode.RAW:
@@ -254,20 +405,6 @@ class RequestArea(Static):
254
405
  else:
255
406
  self.body_enabled_switch.value = True
256
407
 
257
- @on(Input.Changed, '#options-timeout')
258
- def _on_change_timeout(self, message: Input.Changed) -> None:
259
- new_value = message.value
260
-
261
- if new_value == '':
262
- return
263
-
264
- try:
265
- float(new_value)
266
- except Exception:
267
- self.options_timeout_input.value = (
268
- self.options_timeout_input.value[:-1]
269
- )
270
-
271
408
  def _get_headers(self) -> list[HeaderField]:
272
409
  return [
273
410
  HeaderField(
@@ -278,26 +415,58 @@ class RequestArea(Static):
278
415
  for header_field in self.header_fields.values
279
416
  ]
280
417
 
281
- def _get_query_params(self) -> list[QueryParamField]:
418
+ def _get_params(self) -> list[ParamField]:
282
419
  return [
283
- QueryParamField(
284
- enabled=query_param_field['enabled'],
285
- key=query_param_field['key'],
286
- value=query_param_field['value'],
420
+ ParamField(
421
+ enabled=param_field['enabled'],
422
+ key=param_field['key'],
423
+ value=param_field['value'],
287
424
  )
288
- for query_param_field in self.param_fields.values
425
+ for param_field in self.param_fields.values
289
426
  ]
290
427
 
291
- def _get_body(self) -> RequestAreaData.Body:
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
+ ),
458
+ )
459
+
460
+ def _get_body(self) -> Body:
292
461
  body_send: bool = self.body_enabled_switch.value
293
- body_type: str = BodyMode(self.body_mode_select.value)
462
+ body_mode: str = BodyMode(self.body_mode_select.value)
294
463
 
295
464
  payload = None
296
- if body_type == BodyMode.RAW:
465
+ if body_mode == BodyMode.RAW:
297
466
  payload = self.body_raw_editor.text
298
- elif body_type == BodyMode.FILE:
467
+ elif body_mode == BodyMode.FILE:
299
468
  payload = self.body_file_path_chooser.path
300
- elif body_type == BodyMode.FORM_URLENCODED:
469
+ elif body_mode == BodyMode.FORM_URLENCODED:
301
470
  payload = []
302
471
  for form_item in self.body_form_urlencoded_fields.values:
303
472
  payload.append(
@@ -307,7 +476,7 @@ class RequestArea(Static):
307
476
  value=form_item['value'],
308
477
  )
309
478
  )
310
- elif body_type == BodyMode.FORM_MULTIPART:
479
+ elif body_mode == BodyMode.FORM_MULTIPART:
311
480
  payload = []
312
481
  for form_item in self.body_form_multipart_fields.values:
313
482
  payload.append(
@@ -318,19 +487,20 @@ class RequestArea(Static):
318
487
  )
319
488
  )
320
489
 
321
- return RequestAreaData.Body(
490
+ return Body(
322
491
  enabled=body_send,
323
492
  raw_language=BodyRawLanguage(self.body_raw_language_select.value),
324
- type=body_type,
493
+ mode=body_mode,
325
494
  payload=payload,
326
495
  )
327
496
 
328
- def _get_options(self) -> RequestAreaData.Options:
329
- timeout = None
330
- if self.options_timeout_input.value:
497
+ def _get_options(self) -> Options:
498
+ try:
331
499
  timeout = float(self.options_timeout_input.value)
500
+ except ValueError:
501
+ timeout = None
332
502
 
333
- return RequestAreaData.Options(
503
+ return Options(
334
504
  timeout=timeout,
335
505
  follow_redirects=self.options_follow_redirects_switch.value,
336
506
  verify_ssl=self.options_verify_ssl_switch.value,
restiny/enums.py CHANGED
@@ -41,3 +41,10 @@ class ContentType(StrEnum):
41
41
  XML = 'application/xml'
42
42
  FORM_URLENCODED = 'application/x-www-form-urlencoded'
43
43
  FORM_MULTIPART = 'multipart/form-data'
44
+
45
+
46
+ class AuthMode(StrEnum):
47
+ BASIC = 'basic'
48
+ BEARER = 'bearer'
49
+ API_KEY = 'api_key'
50
+ DIGEST = 'digest'
restiny/httpx_auths.py ADDED
@@ -0,0 +1,52 @@
1
+ from collections.abc import Generator
2
+
3
+ import httpx
4
+
5
+
6
+ class BearerAuth(httpx.Auth):
7
+ """
8
+ Adds a Bearer token to the Authorization header of each request.
9
+ """
10
+
11
+ def __init__(self, token: str) -> None:
12
+ self._token = token
13
+
14
+ def auth_flow(
15
+ self, request: httpx.Request
16
+ ) -> Generator[httpx.Request, httpx.Response]:
17
+ request.headers['authorization'] = f'Bearer {self._token}'
18
+ yield request
19
+
20
+
21
+ class APIKeyHeaderAuth(httpx.Auth):
22
+ """
23
+ Adds an API key to the request headers.
24
+ """
25
+
26
+ def __init__(self, key: str, value: str) -> None:
27
+ self._key = key
28
+ self._value = value
29
+
30
+ def auth_flow(
31
+ self, request: httpx.Request
32
+ ) -> Generator[httpx.Request, httpx.Response]:
33
+ request.headers[self._key] = self._value
34
+ yield request
35
+
36
+
37
+ class APIKeyParamAuth(httpx.Auth):
38
+ """
39
+ Adds an API key as a query parameter to the request URL.
40
+ """
41
+
42
+ def __init__(self, key: str, value: str) -> None:
43
+ self._key = key
44
+ self._value = value
45
+
46
+ def auth_flow(
47
+ self, request: httpx.Request
48
+ ) -> Generator[httpx.Request, httpx.Response]:
49
+ request.url = request.url.copy_with(
50
+ params=request.url.params.set(self._key, self._value)
51
+ )
52
+ yield request
restiny/test.py ADDED
@@ -0,0 +1,13 @@
1
+ import random
2
+
3
+
4
+ def test(aaa={}):
5
+ aaa[random.randint(1, 10)] = random.randint(1, 10)
6
+ print(aaa)
7
+
8
+
9
+ test()
10
+ test()
11
+ test()
12
+ test()
13
+ test()
restiny/utils.py CHANGED
@@ -8,32 +8,43 @@ import httpx
8
8
  def build_curl_cmd(
9
9
  method: str,
10
10
  url: str,
11
- headers: dict[str, str] = {},
12
- params: dict[str, str] = {},
13
- raw_body: str | None = None,
14
- form_urlencoded: dict[str, str] = {},
15
- form_multipart: dict[str, str | Path] = {},
16
- files: list[Path] = [],
11
+ headers: dict[str, str] | None = None,
12
+ params: dict[str, str] | None = None,
13
+ body_raw: str | None = None,
14
+ body_form_urlencoded: dict[str, str] | None = None,
15
+ body_form_multipart: dict[str, str | Path] | None = None,
16
+ body_files: list[Path] | None = None,
17
+ auth_basic: tuple[str, str] | None = None,
18
+ auth_bearer: str | None = None,
19
+ auth_api_key_header: tuple[str, str] | None = None,
20
+ auth_api_key_param: tuple[str, str] | None = None,
21
+ auth_digest: tuple[str, str] | None = None,
17
22
  ) -> str:
18
23
  cmd_parts = ['curl']
24
+
25
+ # Method
19
26
  cmd_parts.extend(['--request', method])
20
27
 
21
- url = str(httpx.URL(url).copy_merge_params(params))
28
+ # URL + Params
29
+ if params:
30
+ url = str(httpx.URL(url).copy_merge_params(params))
22
31
  cmd_parts.extend(['--url', shlex.quote(url)])
23
32
 
33
+ # Headers
24
34
  for header_key, header_value in headers.items():
25
35
  header = f'{header_key}: {header_value}'
26
36
  cmd_parts.extend(['--header', shlex.quote(header)])
27
37
 
28
- if raw_body:
29
- cmd_parts.extend(['--data', shlex.quote(raw_body)])
30
- elif form_urlencoded:
31
- for form_key, form_value in form_urlencoded.items():
38
+ # Body
39
+ if body_raw:
40
+ cmd_parts.extend(['--data', shlex.quote(body_raw)])
41
+ elif body_form_urlencoded:
42
+ for form_key, form_value in body_form_urlencoded.items():
32
43
  cmd_parts.extend(
33
44
  ['--data', shlex.quote(f'{form_key}={form_value}')]
34
45
  )
35
- elif form_multipart:
36
- for form_key, form_value in form_multipart.items():
46
+ elif body_form_multipart:
47
+ for form_key, form_value in body_form_multipart.items():
37
48
  if isinstance(form_value, str):
38
49
  cmd_parts.extend(
39
50
  ['--form', shlex.quote(f'{form_key}={form_value}')]
@@ -42,10 +53,29 @@ def build_curl_cmd(
42
53
  cmd_parts.extend(
43
54
  ['--form', shlex.quote(f'{form_key}=@{form_value}')]
44
55
  )
45
- elif files:
46
- for file in files:
56
+ elif body_files:
57
+ for file in body_files:
47
58
  cmd_parts.extend(['--data', shlex.quote(f'@{file}')])
48
59
 
60
+ # Auth
61
+ if auth_basic:
62
+ user, pwd = auth_basic
63
+ cmd_parts.extend(['--user', shlex.quote(f'{user}:{pwd}')])
64
+ elif auth_bearer:
65
+ token = auth_bearer
66
+ cmd_parts.extend(['--header', shlex.quote(f'Authorization: {token}')])
67
+ elif auth_api_key_header:
68
+ key, value = auth_api_key_header
69
+ cmd_parts.extend(['--header', shlex.quote(f'{key}: {value}')])
70
+ elif auth_api_key_param:
71
+ key, value = auth_api_key_param
72
+ url_arg_index = cmd_parts.index('--url')
73
+ new_url = str(httpx.URL(url).copy_merge_params({key: value}))
74
+ cmd_parts[url_arg_index + 1] = shlex.quote(new_url)
75
+ elif auth_digest:
76
+ user, pwd = auth_digest
77
+ cmd_parts.extend(['--digest', '--user', shlex.quote(f'{user}:{pwd}')])
78
+
49
79
  return ' '.join(cmd_parts)
50
80
 
51
81
 
@@ -5,6 +5,7 @@ This module contains reusable widgets used in the DataFox interface.
5
5
  from restiny.widgets.custom_directory_tree import CustomDirectoryTree
6
6
  from restiny.widgets.custom_text_area import CustomTextArea
7
7
  from restiny.widgets.dynamic_fields import DynamicFields, TextDynamicField
8
+ from restiny.widgets.password_input import PasswordInput
8
9
  from restiny.widgets.path_chooser import PathChooser
9
10
 
10
11
  __all__ = [
@@ -13,4 +14,5 @@ __all__ = [
13
14
  'CustomDirectoryTree',
14
15
  'CustomTextArea',
15
16
  'PathChooser',
17
+ 'PasswordInput',
16
18
  ]
@@ -0,0 +1,159 @@
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, Input
9
+
10
+
11
+ class _Icon(StrEnum):
12
+ SHOW = ' 🔓 '
13
+ HIDE = ' 🔒 '
14
+
15
+
16
+ class _Tooltip(StrEnum):
17
+ SHOW = 'Show'
18
+ HIDE = 'Hide'
19
+
20
+
21
+ class PasswordInput(Widget):
22
+ DEFAULT_CSS = """
23
+ PasswordInput {
24
+ width: 1fr;
25
+ height: auto;
26
+ }
27
+
28
+ PasswordInput > Horizontal {
29
+ width: auto;
30
+ height: auto;
31
+ }
32
+
33
+ PasswordInput Input {
34
+ width: 1fr;
35
+ margin-right: 0;
36
+ border-right: none;
37
+ }
38
+
39
+ PasswordInput Input:focus {
40
+ border-right: none;
41
+ }
42
+
43
+
44
+ PasswordInput Button {
45
+ width: auto;
46
+ margin-left: 0;
47
+ border-left: none;
48
+ }
49
+
50
+ """
51
+
52
+ class Changed(Message):
53
+ """
54
+ Sent when value changed.
55
+ """
56
+
57
+ def __init__(self, input: 'PasswordInput', value: str):
58
+ super().__init__()
59
+ self.input = input
60
+ self.value = value
61
+
62
+ @property
63
+ def control(self) -> 'PasswordInput':
64
+ return self.input
65
+
66
+ class Shown(Message):
67
+ """
68
+ Sent when the value becomes visible.
69
+ """
70
+
71
+ def __init__(self, input: 'PasswordInput') -> None:
72
+ super().__init__()
73
+ self.input = input
74
+
75
+ @property
76
+ def control(self) -> 'PasswordInput':
77
+ return self.input
78
+
79
+ class Hidden(Message):
80
+ """
81
+ Sent when the value becomes hidden.
82
+ """
83
+
84
+ def __init__(self, input: 'PasswordInput') -> None:
85
+ super().__init__()
86
+ self.input = input
87
+
88
+ @property
89
+ def control(self) -> 'PasswordInput':
90
+ return self.input
91
+
92
+ def __init__(self, *args, **kwargs) -> None:
93
+ super().__init__(
94
+ id=kwargs.pop('id', None), classes=kwargs.pop('classes', None)
95
+ )
96
+ kwargs.pop('password', None)
97
+ self._input_args = args
98
+ self._input_kwargs = kwargs
99
+
100
+ def compose(self) -> ComposeResult:
101
+ with Horizontal():
102
+ yield Input(
103
+ *self._input_args,
104
+ **self._input_kwargs,
105
+ password=True,
106
+ id='value',
107
+ )
108
+ yield Button(
109
+ _Icon.SHOW, tooltip=_Tooltip.SHOW, id='toggle-visibility'
110
+ )
111
+
112
+ def on_mount(self) -> None:
113
+ self.value_input = self.query_one('#value', Input)
114
+ self.toggle_visibility_button = self.query_one(
115
+ '#toggle-visibility', Button
116
+ )
117
+
118
+ def show(self) -> None:
119
+ self.value_input.password = False
120
+ self.toggle_visibility_button.label = _Icon.HIDE
121
+ self.toggle_visibility_button.tooltip = _Tooltip.HIDE
122
+ self.post_message(message=self.Hidden(input=self))
123
+
124
+ def hide(self) -> None:
125
+ self.value_input.password = True
126
+ self.toggle_visibility_button.label = _Icon.SHOW
127
+ self.toggle_visibility_button.tooltip = _Tooltip.SHOW
128
+ self.post_message(message=self.Shown(input=self))
129
+
130
+ @property
131
+ def value(self) -> str:
132
+ return self.value_input.value
133
+
134
+ @value.setter
135
+ def value(self, value: str) -> None:
136
+ self.value_input.value = value
137
+
138
+ @property
139
+ def shown(self) -> bool:
140
+ return self.value_input.password is False
141
+
142
+ @property
143
+ def hidden(self) -> bool:
144
+ return not self.shown
145
+
146
+ @on(Input.Changed, '#value')
147
+ def _on_value_changed(self, message: Input.Changed) -> None:
148
+ self.post_message(
149
+ message=self.Changed(input=self, value=message.value)
150
+ )
151
+
152
+ @on(Button.Pressed, '#toggle-visibility')
153
+ def _on_toggle_visibility(self, message: Button.Pressed) -> None:
154
+ if self.value_input.password is False:
155
+ self.hide()
156
+ elif self.value_input.password is True:
157
+ self.show()
158
+
159
+ self.value_input.focus()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: restiny
3
- Version: 0.2.1
3
+ Version: 0.3.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
@@ -254,7 +254,7 @@ Dynamic: license-file
254
254
 
255
255
  _A minimal, elegant HTTP client for Python — with a TUI interface powered by [Textual](https://github.com/Textualize/textual)._
256
256
 
257
- ![image](https://github.com/user-attachments/assets/e6f0c03a-e98e-40cd-af1d-38489d650fb1)
257
+ ![image](https://github.com/user-attachments/assets/798c994f-af7e-4be6-8f8c-87157dcf94e0)
258
258
 
259
259
  ## How to install
260
260
 
@@ -0,0 +1,27 @@
1
+ restiny/__about__.py,sha256=3wVEs2QD_7OcTlD97cZdCeizd2hUbJJ0GeIO8wQIjrk,22
2
+ restiny/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ restiny/__main__.py,sha256=omMWZ9xgxXbDE6nyVhE8AnVkohPaXhTAn6cYx6OeRMk,609
4
+ restiny/consts.py,sha256=brPBT5j_Yf7lVPo6mVRPPNUHxGnfDfkLWC4Xr_y3UWo,237
5
+ restiny/enums.py,sha256=IMUMubaZaKpF-7_IAX6XqnumtXrjkK2MNYZRJrB0gH0,988
6
+ restiny/httpx_auths.py,sha256=FqwO6W2AYNhmG5OEbQBusumH-zVvel4A8Oa7c4cbm-4,1295
7
+ restiny/test.py,sha256=ibamS6hhuCg4BYX4NuhtwmelsdnkIdYaFtQr-uC_3GE,141
8
+ restiny/utils.py,sha256=RD_2Q1UPtx9uUN0SPRR6BH0Jk0cm_QFgec-bvCdRSe8,4303
9
+ restiny/assets/__init__.py,sha256=JL1KARlToF6ZR7KeUjlDAHgwwVM2qXYaIl4wHeFW2zU,93
10
+ restiny/assets/style.tcss,sha256=wEHSgnl-gKUFEGahXCQ8Rf7GkvtUR5G8XDqJhehol-4,648
11
+ restiny/core/__init__.py,sha256=qEyvxrQifEiazkiGoaaJwVhKgbXqVu-Y75M-2HWG73U,373
12
+ restiny/core/app.py,sha256=Sw_iXfp8sLTUnsCyLqcs__vXVLdL0xa8NsnvYlgtagA,15618
13
+ restiny/core/request_area.py,sha256=X1RFyyRCQNGzsxAzV-mrzjvoevQIp5-LzANzdviIzV8,17667
14
+ restiny/core/response_area.py,sha256=Rzq82muK9gXWHIHdLqetFG403n7JdE65Rka0Siw339I,4195
15
+ restiny/core/url_area.py,sha256=-DIiNrBSUsIPDMb7TZI_PiG-fM-Karhwv-vwui89g8I,3298
16
+ restiny/widgets/__init__.py,sha256=uWSOTW_3zIbvdkdO-UCzAMFBNn4gF2AsUmuVjyn5V18,542
17
+ restiny/widgets/custom_directory_tree.py,sha256=sNTaI0DBAO56MyOy6qMZPgWXiTUQbBrJdn1GtOdxrDc,1268
18
+ restiny/widgets/custom_text_area.py,sha256=ykmG-6MiMhz6BqNzP8f14jUTWWKjsCOIEhgciP-01Y8,14032
19
+ restiny/widgets/dynamic_fields.py,sha256=tqRE8PkfqTQnYA7RwjFIWpTINryp2NqXqWbmtRYlcvE,16283
20
+ restiny/widgets/password_input.py,sha256=UAGj7nZUKBCPsWhBCcujY-nvwdMWo2zADJgfqDUuiTU,3987
21
+ restiny/widgets/path_chooser.py,sha256=hW7KdKOisxwsMSKDLRn7aEj2-7VXLyzDGdN6m6JR7cI,9212
22
+ restiny-0.3.0.dist-info/licenses/LICENSE,sha256=Z190MKguypkrjaCldiorEbMmBQp7ylvx09oyE4oDCTs,11361
23
+ restiny-0.3.0.dist-info/METADATA,sha256=uQSqnxN32MEPOWWOJ4pxuJzbfwmo0mhdx1thhiLHKCE,16126
24
+ restiny-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ restiny-0.3.0.dist-info/entry_points.txt,sha256=F9zW8bAPAwIihltqjzYow4ahmH_B6VkAHzQFA-8QOn4,50
26
+ restiny-0.3.0.dist-info/top_level.txt,sha256=1MQ_Q-fV1Dwbu4zU3g1Eg-CfRgC412X-mvMIrEdrlbk,8
27
+ restiny-0.3.0.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- restiny/__about__.py,sha256=PmcQ2PI2oP8irnLtJLJby2YfW6sBvLAmL-VpABzTqwc,22
2
- restiny/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- restiny/__main__.py,sha256=omMWZ9xgxXbDE6nyVhE8AnVkohPaXhTAn6cYx6OeRMk,609
4
- restiny/consts.py,sha256=brPBT5j_Yf7lVPo6mVRPPNUHxGnfDfkLWC4Xr_y3UWo,237
5
- restiny/enums.py,sha256=24ApCF1KoGfu-0XpIT_dbZrnOQx7SjmUKFbt5aMa_dg,873
6
- restiny/utils.py,sha256=AD8mtM-AuWhcXbTk3-j9gZWs8SoVnI5SbbUw0cnYEUw,3113
7
- restiny/assets/__init__.py,sha256=JL1KARlToF6ZR7KeUjlDAHgwwVM2qXYaIl4wHeFW2zU,93
8
- restiny/assets/style.tcss,sha256=qq8kLab6TuaDNk7V3El9FzAVb1tjVr3JzYSjBbKwPzM,563
9
- restiny/core/__init__.py,sha256=qEyvxrQifEiazkiGoaaJwVhKgbXqVu-Y75M-2HWG73U,373
10
- restiny/core/app.py,sha256=_chnNKCcuJ0U6XIlvTxotVZM0AYprHaSO-qBy4x1L_Q,12591
11
- restiny/core/request_area.py,sha256=aeWFRZ49h33bxeWt7-XNmQGrxpTnO2cEQ4fewseLm6Q,11570
12
- restiny/core/response_area.py,sha256=Rzq82muK9gXWHIHdLqetFG403n7JdE65Rka0Siw339I,4195
13
- restiny/core/url_area.py,sha256=-DIiNrBSUsIPDMb7TZI_PiG-fM-Karhwv-vwui89g8I,3298
14
- restiny/widgets/__init__.py,sha256=ncQ1uFdIoSuQ2DYmsMxGdu3D4Sf9mMiM1VweDHJuDNQ,464
15
- restiny/widgets/custom_directory_tree.py,sha256=sNTaI0DBAO56MyOy6qMZPgWXiTUQbBrJdn1GtOdxrDc,1268
16
- restiny/widgets/custom_text_area.py,sha256=ykmG-6MiMhz6BqNzP8f14jUTWWKjsCOIEhgciP-01Y8,14032
17
- restiny/widgets/dynamic_fields.py,sha256=tqRE8PkfqTQnYA7RwjFIWpTINryp2NqXqWbmtRYlcvE,16283
18
- restiny/widgets/path_chooser.py,sha256=hW7KdKOisxwsMSKDLRn7aEj2-7VXLyzDGdN6m6JR7cI,9212
19
- restiny-0.2.1.dist-info/licenses/LICENSE,sha256=Z190MKguypkrjaCldiorEbMmBQp7ylvx09oyE4oDCTs,11361
20
- restiny-0.2.1.dist-info/METADATA,sha256=n9GcZmgSaPmnl5Us-CUJVslSIddoGB3WrYdpz_VBnHI,16126
21
- restiny-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- restiny-0.2.1.dist-info/entry_points.txt,sha256=F9zW8bAPAwIihltqjzYow4ahmH_B6VkAHzQFA-8QOn4,50
23
- restiny-0.2.1.dist-info/top_level.txt,sha256=1MQ_Q-fV1Dwbu4zU3g1Eg-CfRgC412X-mvMIrEdrlbk,8
24
- restiny-0.2.1.dist-info/RECORD,,