restiny 0.2.1__py3-none-any.whl → 0.5.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.
- restiny/__about__.py +1 -1
- restiny/__main__.py +26 -14
- restiny/assets/style.tcss +47 -1
- restiny/consts.py +236 -0
- restiny/data/db.py +60 -0
- restiny/data/models.py +90 -0
- restiny/data/repos.py +351 -0
- restiny/data/sql/__init__.py +3 -0
- restiny/entities.py +320 -0
- restiny/enums.py +14 -5
- restiny/httpx_auths.py +52 -0
- restiny/ui/__init__.py +15 -0
- restiny/ui/app.py +500 -0
- restiny/ui/collections_area.py +569 -0
- restiny/ui/request_area.py +575 -0
- restiny/ui/settings_screen.py +80 -0
- restiny/{core → ui}/url_area.py +50 -38
- restiny/utils.py +52 -15
- restiny/widgets/__init__.py +15 -1
- restiny/widgets/collections_tree.py +70 -0
- restiny/widgets/confirm_prompt.py +76 -0
- restiny/widgets/custom_input.py +20 -0
- restiny/widgets/dynamic_fields.py +65 -70
- restiny/widgets/password_input.py +161 -0
- restiny/widgets/path_chooser.py +12 -12
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/METADATA +7 -5
- restiny-0.5.0.dist-info/RECORD +36 -0
- restiny/core/__init__.py +0 -15
- restiny/core/app.py +0 -348
- restiny/core/request_area.py +0 -337
- restiny-0.2.1.dist-info/RECORD +0 -24
- /restiny/{core → ui}/response_area.py +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/WHEEL +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/entry_points.txt +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/top_level.txt +0 -0
restiny/data/repos.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import select
|
|
12
|
+
from sqlalchemy.exc import IntegrityError, InterfaceError, OperationalError
|
|
13
|
+
|
|
14
|
+
from restiny.data.db import DBManager
|
|
15
|
+
from restiny.data.models import SQLFolder, SQLRequest, SQLSettings
|
|
16
|
+
from restiny.entities import Folder, Request, Settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def safe_repo(func):
|
|
20
|
+
@wraps(func)
|
|
21
|
+
def wrapper(*args, **kwargs):
|
|
22
|
+
try:
|
|
23
|
+
return func(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
except (
|
|
26
|
+
InterfaceError,
|
|
27
|
+
OperationalError,
|
|
28
|
+
):
|
|
29
|
+
traceback.print_exc(file=sys.stderr)
|
|
30
|
+
return RepoResp(status=RepoStatus.DB_ERROR)
|
|
31
|
+
|
|
32
|
+
except IntegrityError as error:
|
|
33
|
+
error_msg = str(error)
|
|
34
|
+
if 'UNIQUE' in str(error_msg):
|
|
35
|
+
return RepoResp(status=RepoStatus.DUPLICATED)
|
|
36
|
+
|
|
37
|
+
traceback.print_exc(file=sys.stderr)
|
|
38
|
+
return RepoResp(status=RepoStatus.DB_ERROR)
|
|
39
|
+
|
|
40
|
+
return wrapper
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RepoStatus(StrEnum):
|
|
44
|
+
OK = 'ok'
|
|
45
|
+
NOT_FOUND = 'not_found'
|
|
46
|
+
DUPLICATED = 'duplicated'
|
|
47
|
+
DB_ERROR = 'db_error'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class RepoResp:
|
|
52
|
+
status: RepoStatus = RepoStatus.OK
|
|
53
|
+
data: Any = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def ok(self) -> bool:
|
|
57
|
+
return self.status == RepoStatus.OK
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SQLRepoBase(ABC):
|
|
61
|
+
def __init__(self, db_manager: DBManager):
|
|
62
|
+
self.db_manager = db_manager
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def _updatable_sql_fields(self) -> list[str]:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FoldersSQLRepo(SQLRepoBase):
|
|
71
|
+
@property
|
|
72
|
+
def _updatable_sql_fields(self) -> list[str]:
|
|
73
|
+
return [SQLFolder.parent_id.key, SQLFolder.name.key]
|
|
74
|
+
|
|
75
|
+
@safe_repo
|
|
76
|
+
def list_by_parent_id(self, parent_id: int) -> RepoResp:
|
|
77
|
+
with self.db_manager.session_scope() as session:
|
|
78
|
+
sql_folders = session.scalars(
|
|
79
|
+
select(SQLFolder)
|
|
80
|
+
.where(SQLFolder.parent_id == parent_id)
|
|
81
|
+
.order_by(SQLFolder.name.asc())
|
|
82
|
+
).all()
|
|
83
|
+
folders = [
|
|
84
|
+
self._sql_to_folder(sql_folder) for sql_folder in sql_folders
|
|
85
|
+
]
|
|
86
|
+
return RepoResp(data=folders)
|
|
87
|
+
|
|
88
|
+
@safe_repo
|
|
89
|
+
def list_roots(self) -> RepoResp:
|
|
90
|
+
with self.db_manager.session_scope() as session:
|
|
91
|
+
sql_folders = session.scalars(
|
|
92
|
+
select(SQLFolder)
|
|
93
|
+
.where(SQLFolder.parent_id.is_(None))
|
|
94
|
+
.order_by(SQLFolder.name.asc())
|
|
95
|
+
).all()
|
|
96
|
+
folders = [
|
|
97
|
+
self._sql_to_folder(sql_folder) for sql_folder in sql_folders
|
|
98
|
+
]
|
|
99
|
+
return RepoResp(data=folders)
|
|
100
|
+
|
|
101
|
+
@safe_repo
|
|
102
|
+
def get_by_id(self, id: int) -> RepoResp:
|
|
103
|
+
with self.db_manager.session_scope() as session:
|
|
104
|
+
sql_folder = session.get(SQLFolder, id)
|
|
105
|
+
if not sql_folder:
|
|
106
|
+
return RepoResp(status=RepoStatus.NOT_FOUND)
|
|
107
|
+
|
|
108
|
+
folder = self._sql_to_folder(sql_folder)
|
|
109
|
+
return RepoResp(data=folder)
|
|
110
|
+
|
|
111
|
+
@safe_repo
|
|
112
|
+
def create(self, folder: Folder) -> RepoResp:
|
|
113
|
+
with self.db_manager.session_scope() as session:
|
|
114
|
+
sql_folder = self._folder_to_sql(folder)
|
|
115
|
+
session.add(sql_folder)
|
|
116
|
+
session.flush()
|
|
117
|
+
new_folder = self._sql_to_folder(sql_folder)
|
|
118
|
+
return RepoResp(data=new_folder)
|
|
119
|
+
|
|
120
|
+
@safe_repo
|
|
121
|
+
def update(self, folder: Folder) -> RepoResp:
|
|
122
|
+
with self.db_manager.session_scope() as session:
|
|
123
|
+
sql_folder = session.get(SQLFolder, folder.id)
|
|
124
|
+
if not sql_folder:
|
|
125
|
+
return RepoResp(status=RepoStatus.NOT_FOUND)
|
|
126
|
+
|
|
127
|
+
new_data = self._folder_to_sql(folder)
|
|
128
|
+
for field in self._updatable_sql_fields:
|
|
129
|
+
setattr(sql_folder, field, getattr(new_data, field))
|
|
130
|
+
|
|
131
|
+
session.flush()
|
|
132
|
+
|
|
133
|
+
new_folder = self._sql_to_folder(sql_folder)
|
|
134
|
+
return RepoResp(data=new_folder)
|
|
135
|
+
|
|
136
|
+
@safe_repo
|
|
137
|
+
def delete_by_id(self, id: int) -> RepoResp:
|
|
138
|
+
with self.db_manager.session_scope() as session:
|
|
139
|
+
sql_folder = session.get(SQLFolder, id)
|
|
140
|
+
if not sql_folder:
|
|
141
|
+
return RepoResp(status=RepoStatus.NOT_FOUND)
|
|
142
|
+
|
|
143
|
+
session.delete(sql_folder)
|
|
144
|
+
return RepoResp()
|
|
145
|
+
|
|
146
|
+
def _sql_to_folder(self, sql_folder: SQLFolder) -> Folder:
|
|
147
|
+
return Folder(
|
|
148
|
+
id=sql_folder.id,
|
|
149
|
+
parent_id=sql_folder.parent_id,
|
|
150
|
+
name=sql_folder.name,
|
|
151
|
+
created_at=sql_folder.created_at.replace(tzinfo=UTC),
|
|
152
|
+
updated_at=sql_folder.updated_at.replace(tzinfo=UTC),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _folder_to_sql(self, folder: Folder) -> SQLFolder:
|
|
156
|
+
return SQLFolder(
|
|
157
|
+
id=folder.id,
|
|
158
|
+
parent_id=folder.parent_id,
|
|
159
|
+
name=folder.name,
|
|
160
|
+
created_at=folder.created_at,
|
|
161
|
+
updated_at=folder.updated_at,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class RequestsSQLRepo(SQLRepoBase):
|
|
166
|
+
@safe_repo
|
|
167
|
+
def list_by_folder_id(self, folder_id: int) -> RepoResp:
|
|
168
|
+
with self.db_manager.session_scope() as session:
|
|
169
|
+
sql_requests = session.scalars(
|
|
170
|
+
select(SQLRequest)
|
|
171
|
+
.where(SQLRequest.folder_id == folder_id)
|
|
172
|
+
.order_by(SQLRequest.name.asc())
|
|
173
|
+
).all()
|
|
174
|
+
requests = [
|
|
175
|
+
self._sql_to_request(sql_folder) for sql_folder in sql_requests
|
|
176
|
+
]
|
|
177
|
+
return RepoResp(data=requests)
|
|
178
|
+
|
|
179
|
+
@safe_repo
|
|
180
|
+
def get_by_id(self, id: int) -> RepoResp:
|
|
181
|
+
with self.db_manager.session_scope() as session:
|
|
182
|
+
sql_request = session.get(SQLRequest, id)
|
|
183
|
+
|
|
184
|
+
if not sql_request:
|
|
185
|
+
return RepoResp(status=RepoStatus.NOT_FOUND)
|
|
186
|
+
|
|
187
|
+
request = self._sql_to_request(sql_request)
|
|
188
|
+
return RepoResp(data=request)
|
|
189
|
+
|
|
190
|
+
@safe_repo
|
|
191
|
+
def create(self, request: Request) -> RepoResp:
|
|
192
|
+
with self.db_manager.session_scope() as session:
|
|
193
|
+
sql_request = self._request_to_sql(request)
|
|
194
|
+
session.add(sql_request)
|
|
195
|
+
session.flush()
|
|
196
|
+
new_request = self._sql_to_request(sql_request)
|
|
197
|
+
return RepoResp(data=new_request)
|
|
198
|
+
|
|
199
|
+
@safe_repo
|
|
200
|
+
def update(self, request: Request) -> RepoResp:
|
|
201
|
+
with self.db_manager.session_scope() as session:
|
|
202
|
+
sql_request = session.get(SQLRequest, request.id)
|
|
203
|
+
if not sql_request:
|
|
204
|
+
return RepoResp(status=RepoStatus.NOT_FOUND)
|
|
205
|
+
|
|
206
|
+
new_data = self._request_to_sql(request)
|
|
207
|
+
for field in self._updatable_sql_fields:
|
|
208
|
+
setattr(sql_request, field, getattr(new_data, field))
|
|
209
|
+
|
|
210
|
+
session.flush()
|
|
211
|
+
|
|
212
|
+
new_request = self._sql_to_request(sql_request)
|
|
213
|
+
return RepoResp(data=new_request)
|
|
214
|
+
|
|
215
|
+
@safe_repo
|
|
216
|
+
def delete_by_id(self, id: int) -> RepoResp:
|
|
217
|
+
with self.db_manager.session_scope() as session:
|
|
218
|
+
sql_request = session.get(SQLRequest, id)
|
|
219
|
+
if not sql_request:
|
|
220
|
+
return RepoResp(status=RepoStatus.NOT_FOUND)
|
|
221
|
+
|
|
222
|
+
session.delete(sql_request)
|
|
223
|
+
return RepoResp()
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def _updatable_sql_fields(self) -> list[str]:
|
|
227
|
+
return [
|
|
228
|
+
SQLRequest.folder_id.key,
|
|
229
|
+
SQLRequest.name.key,
|
|
230
|
+
SQLRequest.method.key,
|
|
231
|
+
SQLRequest.url.key,
|
|
232
|
+
SQLRequest.headers.key,
|
|
233
|
+
SQLRequest.params.key,
|
|
234
|
+
SQLRequest.body_enabled.key,
|
|
235
|
+
SQLRequest.body_mode.key,
|
|
236
|
+
SQLRequest.body.key,
|
|
237
|
+
SQLRequest.auth_enabled.key,
|
|
238
|
+
SQLRequest.auth_mode.key,
|
|
239
|
+
SQLRequest.auth.key,
|
|
240
|
+
SQLRequest.option_timeout.key,
|
|
241
|
+
SQLRequest.option_follow_redirects.key,
|
|
242
|
+
SQLRequest.option_verify_ssl.key,
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
def _sql_to_request(self, sql_request: SQLRequest) -> Request:
|
|
246
|
+
return Request(
|
|
247
|
+
id=sql_request.id,
|
|
248
|
+
folder_id=sql_request.folder_id,
|
|
249
|
+
name=sql_request.name,
|
|
250
|
+
method=sql_request.method,
|
|
251
|
+
url=sql_request.url,
|
|
252
|
+
headers=json.loads(sql_request.headers),
|
|
253
|
+
params=json.loads(sql_request.params),
|
|
254
|
+
body_enabled=sql_request.body_enabled,
|
|
255
|
+
body_mode=sql_request.body_mode,
|
|
256
|
+
body=json.loads(sql_request.body) if sql_request.body else None,
|
|
257
|
+
auth_enabled=sql_request.auth_enabled,
|
|
258
|
+
auth_mode=sql_request.auth_mode,
|
|
259
|
+
auth=json.loads(sql_request.auth) if sql_request.auth else None,
|
|
260
|
+
options=Request.Options(
|
|
261
|
+
timeout=sql_request.option_timeout,
|
|
262
|
+
follow_redirects=sql_request.option_follow_redirects,
|
|
263
|
+
verify_ssl=sql_request.option_verify_ssl,
|
|
264
|
+
),
|
|
265
|
+
created_at=sql_request.created_at.replace(tzinfo=UTC),
|
|
266
|
+
updated_at=sql_request.updated_at.replace(tzinfo=UTC),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _request_to_sql(self, request: Request) -> SQLRequest:
|
|
270
|
+
return SQLRequest(
|
|
271
|
+
id=request.id,
|
|
272
|
+
folder_id=request.folder_id,
|
|
273
|
+
name=request.name,
|
|
274
|
+
method=request.method,
|
|
275
|
+
url=request.url,
|
|
276
|
+
headers=json.dumps(
|
|
277
|
+
[header.model_dump() for header in request.headers]
|
|
278
|
+
),
|
|
279
|
+
params=json.dumps(
|
|
280
|
+
[param.model_dump() for param in request.params]
|
|
281
|
+
),
|
|
282
|
+
body_enabled=request.body_enabled,
|
|
283
|
+
body_mode=request.body_mode,
|
|
284
|
+
body=json.dumps(request.body.model_dump(), default=str)
|
|
285
|
+
if request.body
|
|
286
|
+
else None,
|
|
287
|
+
auth_enabled=request.auth_enabled,
|
|
288
|
+
auth_mode=request.auth_mode,
|
|
289
|
+
auth=json.dumps(request.auth.model_dump(), default=str)
|
|
290
|
+
if request.auth
|
|
291
|
+
else None,
|
|
292
|
+
option_timeout=request.options.timeout,
|
|
293
|
+
option_follow_redirects=request.options.follow_redirects,
|
|
294
|
+
option_verify_ssl=request.options.verify_ssl,
|
|
295
|
+
created_at=request.created_at,
|
|
296
|
+
updated_at=request.updated_at,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class SettingsSQLRepo(SQLRepoBase):
|
|
301
|
+
@safe_repo
|
|
302
|
+
def get(self) -> RepoResp:
|
|
303
|
+
with self.db_manager.session_scope() as session:
|
|
304
|
+
sql_settings = session.scalar(select(SQLSettings).limit(1))
|
|
305
|
+
|
|
306
|
+
if not sql_settings:
|
|
307
|
+
return RepoResp(data=Settings())
|
|
308
|
+
|
|
309
|
+
settings = self._sql_to_settings(sql_settings)
|
|
310
|
+
return RepoResp(data=settings)
|
|
311
|
+
|
|
312
|
+
@safe_repo
|
|
313
|
+
def set(self, settings: Settings) -> RepoResp:
|
|
314
|
+
with self.db_manager.session_scope() as session:
|
|
315
|
+
sql_settings = session.scalar(select(SQLSettings).limit(1))
|
|
316
|
+
|
|
317
|
+
if not sql_settings:
|
|
318
|
+
# create
|
|
319
|
+
sql_settings = self._settings_to_sql(settings=settings)
|
|
320
|
+
session.add(sql_settings)
|
|
321
|
+
session.flush()
|
|
322
|
+
new_settings = self._sql_to_settings(sql_settings=sql_settings)
|
|
323
|
+
return RepoResp(data=new_settings)
|
|
324
|
+
else:
|
|
325
|
+
# update
|
|
326
|
+
new_data = self._settings_to_sql(settings=settings)
|
|
327
|
+
for field in self._updatable_sql_fields:
|
|
328
|
+
setattr(sql_settings, field, getattr(new_data, field))
|
|
329
|
+
session.flush()
|
|
330
|
+
new_settings = self._sql_to_settings(sql_settings=sql_settings)
|
|
331
|
+
return RepoResp(data=new_settings)
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def _updatable_sql_fields(self) -> list[str]:
|
|
335
|
+
return [SQLSettings.theme.key]
|
|
336
|
+
|
|
337
|
+
def _sql_to_settings(self, sql_settings: SQLSettings) -> Settings:
|
|
338
|
+
return Settings(
|
|
339
|
+
id=sql_settings.id,
|
|
340
|
+
theme=sql_settings.theme,
|
|
341
|
+
created_at=sql_settings.created_at.replace(tzinfo=UTC),
|
|
342
|
+
updated_at=sql_settings.updated_at.replace(tzinfo=UTC),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def _settings_to_sql(self, settings: Settings) -> SQLSettings:
|
|
346
|
+
return SQLSettings(
|
|
347
|
+
id=settings.id,
|
|
348
|
+
theme=settings.theme,
|
|
349
|
+
created_at=settings.created_at,
|
|
350
|
+
updated_at=settings.updated_at,
|
|
351
|
+
)
|
restiny/entities.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
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 to_httpx_req(self) -> httpx.Request:
|
|
128
|
+
headers: dict[str, str] = {
|
|
129
|
+
header.key: header.value
|
|
130
|
+
for header in self.headers
|
|
131
|
+
if header.enabled
|
|
132
|
+
}
|
|
133
|
+
params: dict[str, str] = {
|
|
134
|
+
param.key: param.value for param in self.params if param.enabled
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if not self.body_enabled:
|
|
138
|
+
return httpx.Request(
|
|
139
|
+
method=self.method,
|
|
140
|
+
url=self.url,
|
|
141
|
+
headers=headers,
|
|
142
|
+
params=params,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if self.body_mode == BodyMode.RAW:
|
|
146
|
+
raw_language_to_content_type = {
|
|
147
|
+
BodyRawLanguage.JSON: ContentType.JSON,
|
|
148
|
+
BodyRawLanguage.YAML: ContentType.YAML,
|
|
149
|
+
BodyRawLanguage.HTML: ContentType.HTML,
|
|
150
|
+
BodyRawLanguage.XML: ContentType.XML,
|
|
151
|
+
BodyRawLanguage.PLAIN: ContentType.TEXT,
|
|
152
|
+
}
|
|
153
|
+
headers['content-type'] = raw_language_to_content_type.get(
|
|
154
|
+
self.body.language, ContentType.TEXT
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
raw = self.body.value
|
|
158
|
+
if headers['content-type'] == ContentType.JSON:
|
|
159
|
+
try:
|
|
160
|
+
raw = json.dumps(raw)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
return httpx.Request(
|
|
165
|
+
method=self.method,
|
|
166
|
+
url=self.url,
|
|
167
|
+
headers=headers,
|
|
168
|
+
params=params,
|
|
169
|
+
content=raw,
|
|
170
|
+
)
|
|
171
|
+
elif self.body_mode == BodyMode.FILE:
|
|
172
|
+
file = self.body.file
|
|
173
|
+
if 'content-type' not in headers:
|
|
174
|
+
headers['content-type'] = (
|
|
175
|
+
mimetypes.guess_type(file.name)[0]
|
|
176
|
+
or 'application/octet-stream'
|
|
177
|
+
)
|
|
178
|
+
return httpx.Request(
|
|
179
|
+
method=self.method,
|
|
180
|
+
url=self.url,
|
|
181
|
+
headers=headers,
|
|
182
|
+
params=params,
|
|
183
|
+
content=file.read_bytes(),
|
|
184
|
+
)
|
|
185
|
+
elif self.body_mode == BodyMode.FORM_URLENCODED:
|
|
186
|
+
form_urlencoded = {
|
|
187
|
+
form_item.key: form_item.value
|
|
188
|
+
for form_item in self.body.fields
|
|
189
|
+
if form_item.enabled
|
|
190
|
+
}
|
|
191
|
+
return httpx.Request(
|
|
192
|
+
method=self.method,
|
|
193
|
+
url=self.url,
|
|
194
|
+
headers=headers,
|
|
195
|
+
params=params,
|
|
196
|
+
data=form_urlencoded,
|
|
197
|
+
)
|
|
198
|
+
elif self.body_mode == BodyMode.FORM_MULTIPART:
|
|
199
|
+
form_multipart_str = {
|
|
200
|
+
form_item.key: form_item.value
|
|
201
|
+
for form_item in self.body.fields
|
|
202
|
+
if form_item.enabled and isinstance(form_item.value, str)
|
|
203
|
+
}
|
|
204
|
+
form_multipart_files = {
|
|
205
|
+
form_item.key: (
|
|
206
|
+
form_item.value.name,
|
|
207
|
+
form_item.value.read_bytes(),
|
|
208
|
+
mimetypes.guess_type(form_item.value.name)[0]
|
|
209
|
+
or 'application/octet-stream',
|
|
210
|
+
)
|
|
211
|
+
for form_item in self.body.fields
|
|
212
|
+
if form_item.enabled and isinstance(form_item.value, Path)
|
|
213
|
+
}
|
|
214
|
+
return httpx.Request(
|
|
215
|
+
method=self.method,
|
|
216
|
+
url=self.url,
|
|
217
|
+
headers=headers,
|
|
218
|
+
params=params,
|
|
219
|
+
data=form_multipart_str,
|
|
220
|
+
files=form_multipart_files,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def to_httpx_auth(self) -> httpx.Auth | None:
|
|
224
|
+
if not self.auth_enabled:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if self.auth_mode == AuthMode.BASIC:
|
|
228
|
+
return httpx.BasicAuth(
|
|
229
|
+
username=self.auth.username, password=self.auth.password
|
|
230
|
+
)
|
|
231
|
+
elif self.auth_mode == AuthMode.BEARER:
|
|
232
|
+
return httpx_auths.BearerAuth(token=self.auth.token)
|
|
233
|
+
elif self.auth_mode == AuthMode.API_KEY:
|
|
234
|
+
if self.auth.where == 'header':
|
|
235
|
+
return httpx_auths.APIKeyHeaderAuth(
|
|
236
|
+
key=self.auth.key, value=self.auth.value
|
|
237
|
+
)
|
|
238
|
+
elif self.auth.where == 'param':
|
|
239
|
+
return httpx_auths.APIKeyParamAuth(
|
|
240
|
+
key=self.auth.key, value=self.auth.value
|
|
241
|
+
)
|
|
242
|
+
elif self.auth_mode == AuthMode.DIGEST:
|
|
243
|
+
return httpx.DigestAuth(
|
|
244
|
+
username=self.auth.username, password=self.auth.password
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def to_curl(self) -> str:
|
|
248
|
+
headers: dict[str, str] = {
|
|
249
|
+
header.key: header.value
|
|
250
|
+
for header in self.headers
|
|
251
|
+
if header.enabled
|
|
252
|
+
}
|
|
253
|
+
params: dict[str, str] = {
|
|
254
|
+
param.key: param.value for param in self.params if param.enabled
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
body_raw = None
|
|
258
|
+
body_form_urlencoded = None
|
|
259
|
+
body_form_multipart = None
|
|
260
|
+
body_files = None
|
|
261
|
+
if self.body_enabled:
|
|
262
|
+
if self.body_mode == BodyMode.RAW:
|
|
263
|
+
body_raw = self.body
|
|
264
|
+
elif self.body_mode == BodyMode.FORM_URLENCODED:
|
|
265
|
+
body_form_urlencoded = {
|
|
266
|
+
form_field.key: form_field.value
|
|
267
|
+
for form_field in self.body.fields
|
|
268
|
+
if form_field.enabled
|
|
269
|
+
}
|
|
270
|
+
elif self.body_mode == BodyMode.FORM_MULTIPART:
|
|
271
|
+
body_form_multipart = {
|
|
272
|
+
form_field.key: form_field.value
|
|
273
|
+
for form_field in self.body.fields
|
|
274
|
+
if form_field.enabled
|
|
275
|
+
}
|
|
276
|
+
elif self.body_mode == BodyMode.FILE:
|
|
277
|
+
body_files = [self.body]
|
|
278
|
+
|
|
279
|
+
auth_basic = None
|
|
280
|
+
auth_bearer = None
|
|
281
|
+
auth_api_key_header = None
|
|
282
|
+
auth_api_key_param = None
|
|
283
|
+
auth_digest = None
|
|
284
|
+
if self.auth_enabled:
|
|
285
|
+
if self.auth_mode == AuthMode.BASIC:
|
|
286
|
+
auth_basic = (self.auth.username, self.auth.password)
|
|
287
|
+
elif self.auth_mode == AuthMode.BEARER:
|
|
288
|
+
auth_bearer = self.auth.token
|
|
289
|
+
elif self.auth_mode == AuthMode.API_KEY:
|
|
290
|
+
if self.auth.where == 'header':
|
|
291
|
+
auth_api_key_header = (self.auth.key, self.auth.value)
|
|
292
|
+
elif self.auth.where == 'param':
|
|
293
|
+
auth_api_key_param = (self.auth.key, self.auth.value)
|
|
294
|
+
elif self.auth_mode == AuthMode.DIGEST:
|
|
295
|
+
auth_digest = (self.auth.username, self.auth.password)
|
|
296
|
+
|
|
297
|
+
return build_curl_cmd(
|
|
298
|
+
method=self.method,
|
|
299
|
+
url=self.url,
|
|
300
|
+
headers=headers,
|
|
301
|
+
params=params,
|
|
302
|
+
body_raw=body_raw,
|
|
303
|
+
body_form_urlencoded=body_form_urlencoded,
|
|
304
|
+
body_form_multipart=body_form_multipart,
|
|
305
|
+
body_files=body_files,
|
|
306
|
+
auth_basic=auth_basic,
|
|
307
|
+
auth_bearer=auth_bearer,
|
|
308
|
+
auth_api_key_header=auth_api_key_header,
|
|
309
|
+
auth_api_key_param=auth_api_key_param,
|
|
310
|
+
auth_digest=auth_digest,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Settings(BaseModel):
|
|
315
|
+
id: int | None = None
|
|
316
|
+
|
|
317
|
+
theme: CustomThemes = CustomThemes.DARK
|
|
318
|
+
|
|
319
|
+
created_at: datetime | None = None
|
|
320
|
+
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'
|