restiny 0.2.1__tar.gz → 0.3.0__tar.gz
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-0.2.1 → restiny-0.3.0}/PKG-INFO +2 -2
- {restiny-0.2.1 → restiny-0.3.0}/README.md +1 -1
- restiny-0.3.0/restiny/__about__.py +1 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/assets/style.tcss +13 -1
- {restiny-0.2.1 → restiny-0.3.0}/restiny/core/app.py +118 -43
- {restiny-0.2.1 → restiny-0.3.0}/restiny/core/request_area.py +229 -59
- {restiny-0.2.1 → restiny-0.3.0}/restiny/enums.py +7 -0
- restiny-0.3.0/restiny/httpx_auths.py +52 -0
- restiny-0.3.0/restiny/test.py +13 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/utils.py +45 -15
- {restiny-0.2.1 → restiny-0.3.0}/restiny/widgets/__init__.py +2 -0
- restiny-0.3.0/restiny/widgets/password_input.py +159 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny.egg-info/PKG-INFO +2 -2
- {restiny-0.2.1 → restiny-0.3.0}/restiny.egg-info/SOURCES.txt +3 -0
- restiny-0.2.1/restiny/__about__.py +0 -1
- {restiny-0.2.1 → restiny-0.3.0}/LICENSE +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/pyproject.toml +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/__init__.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/__main__.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/assets/__init__.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/consts.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/core/__init__.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/core/response_area.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/core/url_area.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/widgets/custom_directory_tree.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/widgets/custom_text_area.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/widgets/dynamic_fields.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny/widgets/path_chooser.py +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny.egg-info/dependency_links.txt +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny.egg-info/entry_points.txt +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny.egg-info/requires.txt +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/restiny.egg-info/top_level.txt +0 -0
- {restiny-0.2.1 → restiny-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: restiny
|
|
3
|
-
Version: 0.
|
|
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
|
-

|
|
258
258
|
|
|
259
259
|
## How to install
|
|
260
260
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
_A minimal, elegant HTTP client for Python — with a TUI interface powered by [Textual](https://github.com/Textualize/textual)._
|
|
17
17
|
|
|
18
|
-

|
|
19
19
|
|
|
20
20
|
## How to install
|
|
21
21
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.3.0'
|
|
@@ -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
|
-
|
|
39
|
+
height: 2fr;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.h-3fr {
|
|
43
|
+
height: 3fr;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
.p-0 {
|
|
@@ -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
|
-
|
|
68
|
-
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if request_area_data.body.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
272
|
+
params: dict[str, str] = {
|
|
229
273
|
param.key: param.value
|
|
230
|
-
for param in request_area_data.
|
|
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=
|
|
283
|
+
params=params,
|
|
240
284
|
)
|
|
241
285
|
|
|
242
|
-
if request_area_data.body.
|
|
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=
|
|
309
|
+
params=params,
|
|
266
310
|
content=raw,
|
|
267
311
|
)
|
|
268
|
-
elif request_area_data.body.
|
|
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=
|
|
323
|
+
params=params,
|
|
280
324
|
content=file.read_bytes(),
|
|
281
325
|
)
|
|
282
|
-
elif request_area_data.body.
|
|
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=
|
|
336
|
+
params=params,
|
|
293
337
|
data=form_urlencoded,
|
|
294
338
|
)
|
|
295
|
-
elif request_area_data.body.
|
|
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=
|
|
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
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
418
|
+
def _get_params(self) -> list[ParamField]:
|
|
282
419
|
return [
|
|
283
|
-
|
|
284
|
-
enabled=
|
|
285
|
-
key=
|
|
286
|
-
value=
|
|
420
|
+
ParamField(
|
|
421
|
+
enabled=param_field['enabled'],
|
|
422
|
+
key=param_field['key'],
|
|
423
|
+
value=param_field['value'],
|
|
287
424
|
)
|
|
288
|
-
for
|
|
425
|
+
for param_field in self.param_fields.values
|
|
289
426
|
]
|
|
290
427
|
|
|
291
|
-
def
|
|
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
|
-
|
|
462
|
+
body_mode: str = BodyMode(self.body_mode_select.value)
|
|
294
463
|
|
|
295
464
|
payload = None
|
|
296
|
-
if
|
|
465
|
+
if body_mode == BodyMode.RAW:
|
|
297
466
|
payload = self.body_raw_editor.text
|
|
298
|
-
elif
|
|
467
|
+
elif body_mode == BodyMode.FILE:
|
|
299
468
|
payload = self.body_file_path_chooser.path
|
|
300
|
-
elif
|
|
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
|
|
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
|
|
490
|
+
return Body(
|
|
322
491
|
enabled=body_send,
|
|
323
492
|
raw_language=BodyRawLanguage(self.body_raw_language_select.value),
|
|
324
|
-
|
|
493
|
+
mode=body_mode,
|
|
325
494
|
payload=payload,
|
|
326
495
|
)
|
|
327
496
|
|
|
328
|
-
def _get_options(self) ->
|
|
329
|
-
|
|
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
|
|
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,
|
|
@@ -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'
|
|
@@ -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
|
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
36
|
-
for form_key, form_value in
|
|
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
|
|
46
|
-
for file in
|
|
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.
|
|
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
|
-

|
|
258
258
|
|
|
259
259
|
## How to install
|
|
260
260
|
|
|
@@ -6,6 +6,8 @@ restiny/__init__.py
|
|
|
6
6
|
restiny/__main__.py
|
|
7
7
|
restiny/consts.py
|
|
8
8
|
restiny/enums.py
|
|
9
|
+
restiny/httpx_auths.py
|
|
10
|
+
restiny/test.py
|
|
9
11
|
restiny/utils.py
|
|
10
12
|
restiny.egg-info/PKG-INFO
|
|
11
13
|
restiny.egg-info/SOURCES.txt
|
|
@@ -24,4 +26,5 @@ restiny/widgets/__init__.py
|
|
|
24
26
|
restiny/widgets/custom_directory_tree.py
|
|
25
27
|
restiny/widgets/custom_text_area.py
|
|
26
28
|
restiny/widgets/dynamic_fields.py
|
|
29
|
+
restiny/widgets/password_input.py
|
|
27
30
|
restiny/widgets/path_chooser.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.2.1'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|