restiny 0.2.1__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. restiny/__about__.py +1 -1
  2. restiny/__main__.py +28 -14
  3. restiny/assets/style.tcss +56 -2
  4. restiny/consts.py +236 -0
  5. restiny/data/db.py +60 -0
  6. restiny/data/models.py +111 -0
  7. restiny/data/repos.py +455 -0
  8. restiny/data/sql/__init__.py +3 -0
  9. restiny/entities.py +438 -0
  10. restiny/enums.py +14 -5
  11. restiny/httpx_auths.py +52 -0
  12. restiny/ui/__init__.py +17 -0
  13. restiny/ui/app.py +586 -0
  14. restiny/ui/collections_area.py +594 -0
  15. restiny/ui/environments_screen.py +270 -0
  16. restiny/ui/request_area.py +602 -0
  17. restiny/{core → ui}/response_area.py +4 -1
  18. restiny/ui/settings_screen.py +73 -0
  19. restiny/ui/top_bar_area.py +60 -0
  20. restiny/{core → ui}/url_area.py +54 -38
  21. restiny/utils.py +52 -15
  22. restiny/widgets/__init__.py +15 -1
  23. restiny/widgets/collections_tree.py +74 -0
  24. restiny/widgets/confirm_prompt.py +76 -0
  25. restiny/widgets/custom_input.py +20 -0
  26. restiny/widgets/dynamic_fields.py +65 -70
  27. restiny/widgets/password_input.py +161 -0
  28. restiny/widgets/path_chooser.py +12 -12
  29. {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/METADATA +7 -5
  30. restiny-0.6.1.dist-info/RECORD +38 -0
  31. restiny/core/__init__.py +0 -15
  32. restiny/core/app.py +0 -348
  33. restiny/core/request_area.py +0 -337
  34. restiny-0.2.1.dist-info/RECORD +0 -24
  35. {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/WHEEL +0 -0
  36. {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/entry_points.txt +0 -0
  37. {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/licenses/LICENSE +0 -0
  38. {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/top_level.txt +0 -0
restiny/entities.py ADDED
@@ -0,0 +1,438 @@
1
+ import json
2
+ import mimetypes
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, Literal
6
+
7
+ import httpx
8
+ from pydantic import BaseModel, field_validator
9
+ from pydantic import Field as _Field
10
+ from pydantic_core.core_schema import ValidationInfo
11
+
12
+ from restiny import httpx_auths
13
+ from restiny.enums import (
14
+ AuthMode,
15
+ BodyMode,
16
+ BodyRawLanguage,
17
+ ContentType,
18
+ CustomThemes,
19
+ HTTPMethod,
20
+ )
21
+ from restiny.utils import build_curl_cmd
22
+
23
+
24
+ class Folder(BaseModel):
25
+ id: int | None = None
26
+
27
+ name: str
28
+ parent_id: int | None = None
29
+
30
+ created_at: datetime | None = None
31
+ updated_at: datetime | None = None
32
+
33
+
34
+ class Request(BaseModel):
35
+ class Header(BaseModel):
36
+ enabled: bool
37
+ key: str
38
+ value: str
39
+
40
+ class Param(BaseModel):
41
+ enabled: bool
42
+ key: str
43
+ value: str
44
+
45
+ class RawBody(BaseModel):
46
+ language: BodyRawLanguage
47
+ value: str
48
+
49
+ class FileBody(BaseModel):
50
+ file: Path | None
51
+
52
+ class UrlEncodedFormBody(BaseModel):
53
+ class Field(BaseModel):
54
+ enabled: bool
55
+ key: str
56
+ value: str
57
+
58
+ fields: list[Field]
59
+
60
+ class MultipartFormBody(BaseModel):
61
+ class Field(BaseModel):
62
+ value_kind: Literal['text', 'file']
63
+ enabled: bool
64
+ key: str
65
+ value: str | Path | None
66
+
67
+ @field_validator('value', mode='before')
68
+ @classmethod
69
+ def validate_value(cls, value: Any, info: ValidationInfo):
70
+ if value is None:
71
+ return None
72
+
73
+ kind = info.data.get('value_kind')
74
+ if kind == 'file':
75
+ return Path(value)
76
+ elif kind == 'text':
77
+ return str(value)
78
+
79
+ fields: list[Field]
80
+
81
+ class BasicAuth(BaseModel):
82
+ username: str
83
+ password: str
84
+
85
+ class BearerAuth(BaseModel):
86
+ token: str
87
+
88
+ class ApiKeyAuth(BaseModel):
89
+ key: str
90
+ value: str
91
+ where: Literal['header', 'param']
92
+
93
+ class DigestAuth(BaseModel):
94
+ username: str
95
+ password: str
96
+
97
+ class Options(BaseModel):
98
+ timeout: float = 5.5
99
+ follow_redirects: bool = True
100
+ verify_ssl: bool = True
101
+
102
+ id: int | None = None
103
+
104
+ folder_id: int
105
+ name: str
106
+
107
+ method: HTTPMethod = HTTPMethod.GET
108
+ url: str = ''
109
+ headers: list[Header] = _Field(default_factory=list)
110
+ params: list[Param] = _Field(default_factory=list)
111
+
112
+ body_enabled: bool = False
113
+ body_mode: str = BodyMode.RAW
114
+ body: (
115
+ RawBody | FileBody | UrlEncodedFormBody | MultipartFormBody | None
116
+ ) = None
117
+
118
+ auth_enabled: bool = False
119
+ auth_mode: AuthMode = AuthMode.BASIC
120
+ auth: BasicAuth | BearerAuth | ApiKeyAuth | DigestAuth | None = None
121
+
122
+ options: Options = _Field(default_factory=Options)
123
+
124
+ created_at: datetime | None = None
125
+ updated_at: datetime | None = None
126
+
127
+ def resolve_variables(
128
+ self, variables: list[Environment.Variable]
129
+ ) -> 'Request':
130
+ def _resolve_variables(value: str) -> str:
131
+ new_value = value
132
+ for variable in variables:
133
+ if not variable.enabled:
134
+ continue
135
+
136
+ new_value = new_value.replace(
137
+ '{{' + variable.key + '}}', variable.value
138
+ )
139
+ new_value = new_value.replace(
140
+ '${' + variable.key + '}', variable.value
141
+ )
142
+ return new_value
143
+
144
+ resolved_url = _resolve_variables(self.url)
145
+
146
+ resolved_headers = [
147
+ self.Header(
148
+ enabled=header.enabled,
149
+ key=_resolve_variables(header.key),
150
+ value=_resolve_variables(header.value),
151
+ )
152
+ for header in self.headers
153
+ ]
154
+
155
+ resolved_params = [
156
+ self.Param(
157
+ enabled=param.enabled,
158
+ key=_resolve_variables(param.key),
159
+ value=_resolve_variables(param.value),
160
+ )
161
+ for param in self.params
162
+ ]
163
+
164
+ resolved_auth = self.auth
165
+ if self.auth_enabled:
166
+ if self.auth_mode == AuthMode.BASIC:
167
+ resolved_auth = self.BasicAuth(
168
+ username=_resolve_variables(self.auth.username),
169
+ password=_resolve_variables(self.auth.password),
170
+ )
171
+ elif self.auth_mode == AuthMode.BEARER:
172
+ resolved_auth = self.BearerAuth(
173
+ token=_resolve_variables(self.auth.token)
174
+ )
175
+ elif self.auth_mode == AuthMode.API_KEY:
176
+ resolved_auth = self.ApiKeyAuth(
177
+ key=_resolve_variables(self.auth.key),
178
+ value=_resolve_variables(self.auth.value),
179
+ where=self.auth.where,
180
+ )
181
+ elif self.auth_mode == AuthMode.DIGEST:
182
+ resolved_auth = self.DigestAuth(
183
+ username=_resolve_variables(self.auth.username),
184
+ password=_resolve_variables(self.auth.password),
185
+ )
186
+
187
+ resolved_body = self.body
188
+ if self.body_enabled:
189
+ if self.body_mode == BodyMode.RAW:
190
+ resolved_body = self.RawBody(
191
+ language=self.body.language,
192
+ value=_resolve_variables(self.body.value),
193
+ )
194
+ elif self.body_mode == BodyMode.FILE:
195
+ pass
196
+ elif self.body_mode == BodyMode.FORM_URLENCODED:
197
+ resolved_body = self.UrlEncodedFormBody(
198
+ fields=[
199
+ self.UrlEncodedFormBody.Field(
200
+ enabled=field.enabled,
201
+ key=_resolve_variables(field.key),
202
+ value=_resolve_variables(field.value),
203
+ )
204
+ for field in self.body.fields
205
+ ]
206
+ )
207
+ elif self.body_mode == BodyMode.FORM_MULTIPART:
208
+ resolved_body = self.MultipartFormBody(
209
+ fields=[
210
+ self.MultipartFormBody.Field(
211
+ value_kind=field.value_kind,
212
+ enabled=field.enabled,
213
+ key=_resolve_variables(field.key),
214
+ value=_resolve_variables(field.value),
215
+ )
216
+ for field in self.body.fields
217
+ ]
218
+ )
219
+
220
+ return self.model_copy(
221
+ update=dict(
222
+ url=resolved_url,
223
+ headers=resolved_headers,
224
+ params=resolved_params,
225
+ body=resolved_body,
226
+ auth=resolved_auth,
227
+ )
228
+ )
229
+
230
+ def to_httpx_req(self) -> httpx.Request:
231
+ headers: dict[str, str] = {
232
+ header.key: header.value
233
+ for header in self.headers
234
+ if header.enabled
235
+ }
236
+ params: dict[str, str] = {
237
+ param.key: param.value for param in self.params if param.enabled
238
+ }
239
+
240
+ if not self.body_enabled:
241
+ return httpx.Request(
242
+ method=self.method,
243
+ url=self.url,
244
+ headers=headers,
245
+ params=params,
246
+ )
247
+
248
+ if self.body_mode == BodyMode.RAW:
249
+ raw_language_to_content_type = {
250
+ BodyRawLanguage.JSON: ContentType.JSON,
251
+ BodyRawLanguage.YAML: ContentType.YAML,
252
+ BodyRawLanguage.HTML: ContentType.HTML,
253
+ BodyRawLanguage.XML: ContentType.XML,
254
+ BodyRawLanguage.PLAIN: ContentType.TEXT,
255
+ }
256
+ headers['content-type'] = raw_language_to_content_type.get(
257
+ self.body.language, ContentType.TEXT
258
+ )
259
+
260
+ raw = self.body.value
261
+ if headers['content-type'] == ContentType.JSON:
262
+ try:
263
+ raw = json.dumps(raw)
264
+ except Exception:
265
+ pass
266
+
267
+ return httpx.Request(
268
+ method=self.method,
269
+ url=self.url,
270
+ headers=headers,
271
+ params=params,
272
+ content=raw,
273
+ )
274
+ elif self.body_mode == BodyMode.FILE:
275
+ file = self.body.file
276
+ if 'content-type' not in headers:
277
+ headers['content-type'] = (
278
+ mimetypes.guess_type(file.name)[0]
279
+ or 'application/octet-stream'
280
+ )
281
+ return httpx.Request(
282
+ method=self.method,
283
+ url=self.url,
284
+ headers=headers,
285
+ params=params,
286
+ content=file.read_bytes(),
287
+ )
288
+ elif self.body_mode == BodyMode.FORM_URLENCODED:
289
+ form_urlencoded = {
290
+ form_item.key: form_item.value
291
+ for form_item in self.body.fields
292
+ if form_item.enabled
293
+ }
294
+ return httpx.Request(
295
+ method=self.method,
296
+ url=self.url,
297
+ headers=headers,
298
+ params=params,
299
+ data=form_urlencoded,
300
+ )
301
+ elif self.body_mode == BodyMode.FORM_MULTIPART:
302
+ form_multipart_str = {
303
+ form_item.key: form_item.value
304
+ for form_item in self.body.fields
305
+ if form_item.enabled and isinstance(form_item.value, str)
306
+ }
307
+ form_multipart_files = {
308
+ form_item.key: (
309
+ form_item.value.name,
310
+ form_item.value.read_bytes(),
311
+ mimetypes.guess_type(form_item.value.name)[0]
312
+ or 'application/octet-stream',
313
+ )
314
+ for form_item in self.body.fields
315
+ if form_item.enabled and isinstance(form_item.value, Path)
316
+ }
317
+ return httpx.Request(
318
+ method=self.method,
319
+ url=self.url,
320
+ headers=headers,
321
+ params=params,
322
+ data=form_multipart_str,
323
+ files=form_multipart_files,
324
+ )
325
+
326
+ def to_httpx_auth(self) -> httpx.Auth | None:
327
+ if not self.auth_enabled:
328
+ return
329
+
330
+ if self.auth_mode == AuthMode.BASIC:
331
+ return httpx.BasicAuth(
332
+ username=self.auth.username, password=self.auth.password
333
+ )
334
+ elif self.auth_mode == AuthMode.BEARER:
335
+ return httpx_auths.BearerAuth(token=self.auth.token)
336
+ elif self.auth_mode == AuthMode.API_KEY:
337
+ if self.auth.where == 'header':
338
+ return httpx_auths.APIKeyHeaderAuth(
339
+ key=self.auth.key, value=self.auth.value
340
+ )
341
+ elif self.auth.where == 'param':
342
+ return httpx_auths.APIKeyParamAuth(
343
+ key=self.auth.key, value=self.auth.value
344
+ )
345
+ elif self.auth_mode == AuthMode.DIGEST:
346
+ return httpx.DigestAuth(
347
+ username=self.auth.username, password=self.auth.password
348
+ )
349
+
350
+ def to_curl(self) -> str:
351
+ headers: dict[str, str] = {
352
+ header.key: header.value
353
+ for header in self.headers
354
+ if header.enabled
355
+ }
356
+ params: dict[str, str] = {
357
+ param.key: param.value for param in self.params if param.enabled
358
+ }
359
+
360
+ body_raw = None
361
+ body_form_urlencoded = None
362
+ body_form_multipart = None
363
+ body_files = None
364
+ if self.body_enabled:
365
+ if self.body_mode == BodyMode.RAW:
366
+ body_raw = self.body.value
367
+ elif self.body_mode == BodyMode.FORM_URLENCODED:
368
+ body_form_urlencoded = {
369
+ form_field.key: form_field.value
370
+ for form_field in self.body.fields
371
+ if form_field.enabled
372
+ }
373
+ elif self.body_mode == BodyMode.FORM_MULTIPART:
374
+ body_form_multipart = {
375
+ form_field.key: form_field.value
376
+ for form_field in self.body.fields
377
+ if form_field.enabled
378
+ }
379
+ elif self.body_mode == BodyMode.FILE:
380
+ body_files = [self.body]
381
+
382
+ auth_basic = None
383
+ auth_bearer = None
384
+ auth_api_key_header = None
385
+ auth_api_key_param = None
386
+ auth_digest = None
387
+ if self.auth_enabled:
388
+ if self.auth_mode == AuthMode.BASIC:
389
+ auth_basic = (self.auth.username, self.auth.password)
390
+ elif self.auth_mode == AuthMode.BEARER:
391
+ auth_bearer = self.auth.token
392
+ elif self.auth_mode == AuthMode.API_KEY:
393
+ if self.auth.where == 'header':
394
+ auth_api_key_header = (self.auth.key, self.auth.value)
395
+ elif self.auth.where == 'param':
396
+ auth_api_key_param = (self.auth.key, self.auth.value)
397
+ elif self.auth_mode == AuthMode.DIGEST:
398
+ auth_digest = (self.auth.username, self.auth.password)
399
+
400
+ return build_curl_cmd(
401
+ method=self.method,
402
+ url=self.url,
403
+ headers=headers,
404
+ params=params,
405
+ body_raw=body_raw,
406
+ body_form_urlencoded=body_form_urlencoded,
407
+ body_form_multipart=body_form_multipart,
408
+ body_files=body_files,
409
+ auth_basic=auth_basic,
410
+ auth_bearer=auth_bearer,
411
+ auth_api_key_header=auth_api_key_header,
412
+ auth_api_key_param=auth_api_key_param,
413
+ auth_digest=auth_digest,
414
+ )
415
+
416
+
417
+ class Settings(BaseModel):
418
+ id: int | None = None
419
+
420
+ theme: CustomThemes = CustomThemes.DARK
421
+
422
+ created_at: datetime | None = None
423
+ updated_at: datetime | None = None
424
+
425
+
426
+ class Environment(BaseModel):
427
+ class Variable(BaseModel):
428
+ enabled: bool
429
+ key: str
430
+ value: str
431
+
432
+ id: int | None = None
433
+
434
+ name: str
435
+ variables: list[Variable] = _Field(default_factory=list)
436
+
437
+ created_at: datetime | None = None
438
+ updated_at: datetime | None = None
restiny/enums.py CHANGED
@@ -6,17 +6,13 @@ class HTTPMethod(StrEnum):
6
6
  GET = 'GET'
7
7
  POST = 'POST'
8
8
  PUT = 'PUT'
9
+ PATCH = 'PATCH'
9
10
  DELETE = 'DELETE'
10
11
  HEAD = 'HEAD'
11
12
  OPTIONS = 'OPTIONS'
12
- PATCH = 'PATCH'
13
13
  CONNECT = 'CONNECT'
14
14
  TRACE = 'TRACE'
15
15
 
16
- @classmethod
17
- def values(cls):
18
- return [method.value for method in cls]
19
-
20
16
 
21
17
  class BodyMode(StrEnum):
22
18
  RAW = 'raw'
@@ -41,3 +37,16 @@ class ContentType(StrEnum):
41
37
  XML = 'application/xml'
42
38
  FORM_URLENCODED = 'application/x-www-form-urlencoded'
43
39
  FORM_MULTIPART = 'multipart/form-data'
40
+
41
+
42
+ class AuthMode(StrEnum):
43
+ BASIC = 'basic'
44
+ BEARER = 'bearer'
45
+ API_KEY = 'api_key'
46
+ DIGEST = 'digest'
47
+
48
+
49
+ class CustomThemes(StrEnum):
50
+ DARK = 'dark'
51
+ DRACULA = 'dracula'
52
+ FOREST = 'forest'
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/ui/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ This module contains the specific sections of the DataFox user interface (UI).
3
+ """
4
+
5
+ from restiny.ui.collections_area import CollectionsArea
6
+ from restiny.ui.request_area import RequestArea
7
+ from restiny.ui.response_area import ResponseArea
8
+ from restiny.ui.top_bar_area import TopBarArea
9
+ from restiny.ui.url_area import URLArea
10
+
11
+ __all__ = [
12
+ 'RequestArea',
13
+ 'ResponseArea',
14
+ 'URLArea',
15
+ 'CollectionsArea',
16
+ 'TopBarArea',
17
+ ]