restiny 0.5.0__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.
restiny/ui/app.py CHANGED
@@ -1,34 +1,42 @@
1
1
  import asyncio
2
+ from collections.abc import Iterable
2
3
  from http import HTTPStatus
3
4
 
4
5
  import httpx
5
6
  import pyperclip
6
7
  from textual import on
7
- from textual.app import App, ComposeResult
8
+ from textual.app import App, ComposeResult, SystemCommand
8
9
  from textual.binding import Binding
9
10
  from textual.containers import Horizontal, Vertical
10
11
  from textual.events import DescendantFocus
12
+ from textual.screen import Screen
11
13
  from textual.widget import Widget
12
14
  from textual.widgets import Footer, Header
13
15
 
14
16
  from restiny.__about__ import __version__
15
17
  from restiny.assets import STYLE_TCSS
16
18
  from restiny.consts import CUSTOM_THEMES
17
- from restiny.data.repos import FoldersSQLRepo, RequestsSQLRepo, SettingsSQLRepo
19
+ from restiny.data.repos import (
20
+ EnvironmentsSQLRepo,
21
+ FoldersSQLRepo,
22
+ RequestsSQLRepo,
23
+ SettingsSQLRepo,
24
+ )
18
25
  from restiny.entities import Request, Settings
19
26
  from restiny.enums import (
20
27
  AuthMode,
21
28
  BodyMode,
22
29
  BodyRawLanguage,
23
30
  ContentType,
24
- CustomThemes,
25
31
  )
26
32
  from restiny.ui import (
27
33
  CollectionsArea,
28
34
  RequestArea,
29
35
  ResponseArea,
36
+ TopBarArea,
30
37
  URLArea,
31
38
  )
39
+ from restiny.ui.environments_screen import EnvironmentsScreen
32
40
  from restiny.ui.response_area import ResponseAreaData
33
41
  from restiny.ui.settings_screen import SettingsScreen
34
42
  from restiny.widgets.custom_text_area import CustomTextArea
@@ -37,18 +45,11 @@ from restiny.widgets.custom_text_area import CustomTextArea
37
45
  class RESTinyApp(App, inherit_bindings=False):
38
46
  TITLE = f'RESTiny v{__version__}'
39
47
  SUB_TITLE = 'Minimal HTTP client, no bullshit'
40
- ENABLE_COMMAND_PALETTE = False
41
48
  CSS_PATH = STYLE_TCSS
42
49
  BINDINGS = [
43
50
  Binding(
44
51
  key='escape', action='quit', description='Quit the app', show=True
45
52
  ),
46
- Binding(
47
- key='ctrl+b',
48
- action='toggle_collections',
49
- description='Toggle collections',
50
- show=True,
51
- ),
52
53
  Binding(
53
54
  key='ctrl+n',
54
55
  action='prompt_add',
@@ -70,13 +71,13 @@ class RESTinyApp(App, inherit_bindings=False):
70
71
  Binding(
71
72
  key='ctrl+s',
72
73
  action='save',
73
- description='Save request',
74
+ description='Save req',
74
75
  show=True,
75
76
  ),
76
77
  Binding(
77
- key='f9',
78
- action='copy_as_curl',
79
- description='Copy as curl',
78
+ key='ctrl+b',
79
+ action='toggle_collections',
80
+ description='Toggle collections',
80
81
  show=True,
81
82
  ),
82
83
  Binding(
@@ -85,20 +86,14 @@ class RESTinyApp(App, inherit_bindings=False):
85
86
  description='Maximize/Minimize area',
86
87
  show=True,
87
88
  ),
88
- Binding(
89
- key='f12',
90
- action='open_settings',
91
- description='Settings',
92
- show=True,
93
- ),
94
89
  ]
95
- # theme = 'textual-dark'
96
90
 
97
91
  def __init__(
98
92
  self,
99
93
  folders_repo: FoldersSQLRepo,
100
94
  requests_repo: RequestsSQLRepo,
101
95
  settings_repo: SettingsSQLRepo,
96
+ environments_repo: EnvironmentsSQLRepo,
102
97
  *args,
103
98
  **kwargs,
104
99
  ) -> None:
@@ -106,18 +101,21 @@ class RESTinyApp(App, inherit_bindings=False):
106
101
  self.folders_repo = folders_repo
107
102
  self.requests_repo = requests_repo
108
103
  self.settings_repo = settings_repo
104
+ self.environments_repo = environments_repo
109
105
 
110
106
  self.active_request_task: asyncio.Task | None = None
111
- self.selected_request: Request | None = None
112
107
  self.last_focused_widget: Widget | None = None
113
108
  self.last_focused_maximizable_area: Widget | None = None
114
109
 
110
+ self._selected_request: Request | None = None
111
+
115
112
  def compose(self) -> ComposeResult:
116
113
  yield Header(show_clock=True)
117
114
  with Horizontal():
118
115
  yield CollectionsArea(classes='w-1fr')
119
116
  with Vertical(classes='w-6fr'):
120
- with Horizontal(classes='h-auto'):
117
+ with Vertical(classes='h-auto'):
118
+ yield TopBarArea()
121
119
  yield URLArea()
122
120
  with Horizontal(classes='h-1fr'):
123
121
  yield RequestArea()
@@ -126,16 +124,35 @@ class RESTinyApp(App, inherit_bindings=False):
126
124
 
127
125
  def on_mount(self) -> None:
128
126
  self.collections_area = self.query_one(CollectionsArea)
127
+ self.top_bar_area = self.query_one(TopBarArea)
129
128
  self.url_area = self.query_one(URLArea)
130
129
  self.request_area = self.query_one(RequestArea)
131
130
  self.response_area = self.query_one(ResponseArea)
132
131
 
133
- self.url_area.disabled = True
134
- self.request_area.disabled = True
132
+ self.selected_request = None
135
133
 
136
134
  self._register_themes()
137
135
  self._apply_settings()
138
136
 
137
+ def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
138
+ yield SystemCommand('Copy as cURL', None, self.action_copy_as_curl)
139
+ yield SystemCommand(
140
+ 'Show/Hide keys and help panel',
141
+ None,
142
+ self.action_toggle_help_panel,
143
+ )
144
+ yield SystemCommand(
145
+ 'Save screenshot',
146
+ None,
147
+ lambda: self.set_timer(0.1, self.deliver_screenshot),
148
+ )
149
+ yield SystemCommand(
150
+ 'Manage environments', None, self.action_manage_envs
151
+ )
152
+ yield SystemCommand(
153
+ 'Manage settings', None, self.action_manage_settings
154
+ )
155
+
139
156
  def action_toggle_collections(self) -> None:
140
157
  if self.collections_area.display:
141
158
  self.collections_area.display = False
@@ -146,12 +163,24 @@ class RESTinyApp(App, inherit_bindings=False):
146
163
  self.collections_area.prompt_add()
147
164
 
148
165
  def action_prompt_update(self) -> None:
166
+ if not self.selected_request:
167
+ self.notify('No request selected', severity='warning')
168
+ return
169
+
149
170
  self.collections_area.prompt_update()
150
171
 
151
172
  def action_prompt_delete(self) -> None:
173
+ if not self.selected_request:
174
+ self.notify('No request selected', severity='warning')
175
+ return
176
+
152
177
  self.collections_area.prompt_delete()
153
178
 
154
179
  def action_save(self) -> None:
180
+ if not self.selected_request:
181
+ self.notify('No request selected', severity='warning')
182
+ return
183
+
155
184
  req = self.get_request()
156
185
  self.requests_repo.update(request=req)
157
186
  self.collections_area._populate_children(
@@ -169,22 +198,28 @@ class RESTinyApp(App, inherit_bindings=False):
169
198
  else:
170
199
  self.screen.maximize(self.last_focused_maximizable_area)
171
200
 
201
+ def action_toggle_help_panel(self) -> None:
202
+ if self.query('HelpPanel'):
203
+ self.action_hide_help_panel()
204
+ else:
205
+ self.action_show_help_panel()
206
+
172
207
  def action_copy_as_curl(self) -> None:
173
208
  if not self.selected_request:
174
209
  self.notify(
175
- 'Select a request before copying as CURL.',
210
+ 'No request selected',
176
211
  severity='warning',
177
212
  )
178
213
  return
179
214
 
180
- request = self.get_request()
215
+ request = self.get_resolved_request()
181
216
  self.copy_to_clipboard(request.to_curl())
182
217
  self.notify(
183
- 'Command CURL copied to clipboard',
218
+ 'CURL command copied to clipboard',
184
219
  severity='information',
185
220
  )
186
221
 
187
- def action_open_settings(self) -> None:
222
+ def action_manage_settings(self) -> None:
188
223
  def on_settings_result(result: dict | None) -> None:
189
224
  if not result:
190
225
  return
@@ -192,15 +227,19 @@ class RESTinyApp(App, inherit_bindings=False):
192
227
  self.settings_repo.set(Settings(theme=result['theme']))
193
228
  self._apply_settings()
194
229
 
195
- settings: Settings = self.settings_repo.get().data
196
230
  self.push_screen(
197
- screen=SettingsScreen(
198
- themes=[theme.value for theme in CustomThemes],
199
- theme=settings.theme,
200
- ),
231
+ screen=SettingsScreen(),
201
232
  callback=on_settings_result,
202
233
  )
203
234
 
235
+ def action_manage_envs(self) -> None:
236
+ def on_manage_environments_result(result) -> None:
237
+ self.top_bar_area.populate()
238
+
239
+ self.push_screen(
240
+ screen=EnvironmentsScreen(), callback=on_manage_environments_result
241
+ )
242
+
204
243
  def copy_to_clipboard(self, text: str) -> None:
205
244
  super().copy_to_clipboard(text)
206
245
  try:
@@ -231,14 +270,26 @@ class RESTinyApp(App, inherit_bindings=False):
231
270
  def _on_request_selected(
232
271
  self, message: CollectionsArea.RequestSelected
233
272
  ) -> None:
234
- self.url_area.disabled = False
235
- self.request_area.disabled = False
236
273
  req = self.requests_repo.get_by_id(id=message.request_id).data
237
274
  self.selected_request = req
238
275
  self.set_request(request=req)
239
- self.response_area.set_data(None)
276
+
277
+ self.response_area.clear()
240
278
  self.response_area.is_showing_response = False
241
279
 
280
+ @on(CollectionsArea.RequestUpdated)
281
+ def _on_request_updated(self, message) -> None:
282
+ req = self.requests_repo.get_by_id(id=message.request_id).data
283
+ self.selected_request = req
284
+
285
+ @on(CollectionsArea.RequestDeleted)
286
+ def _on_request_deleted(self, message) -> None:
287
+ self.selected_request = None
288
+
289
+ @on(CollectionsArea.FolderSelected)
290
+ def _on_folder_selected(self, message) -> None:
291
+ self.selected_request = None
292
+
242
293
  def _apply_settings(self) -> None:
243
294
  settings = self.settings_repo.get().data
244
295
  self.theme = settings.theme
@@ -266,6 +317,27 @@ class RESTinyApp(App, inherit_bindings=False):
266
317
  return widget
267
318
  widget = widget.parent
268
319
 
320
+ @property
321
+ def selected_request(self) -> Request | None:
322
+ return self._selected_request
323
+
324
+ @selected_request.setter
325
+ def selected_request(self, request: Request | None) -> None:
326
+ if request is None:
327
+ self.url_area.clear()
328
+ self.request_area.clear()
329
+ self.response_area.clear()
330
+ self.url_area.disabled = True
331
+ self.request_area.disabled = True
332
+ self.response_area.disabled = True
333
+ self.response_area.is_showing_response = False
334
+ else:
335
+ self.url_area.disabled = False
336
+ self.request_area.disabled = False
337
+ self.response_area.disabled = False
338
+
339
+ self._selected_request = request
340
+
269
341
  def get_request(self) -> Request:
270
342
  method = self.url_area.method
271
343
  url = self.url_area.url
@@ -369,6 +441,20 @@ class RESTinyApp(App, inherit_bindings=False):
369
441
  options=options,
370
442
  )
371
443
 
444
+ def get_resolved_request(self) -> Request:
445
+ global_environment = self.environments_repo.get_by_name(
446
+ name='global'
447
+ ).data
448
+ request = self.get_request().resolve_variables(
449
+ global_environment.variables
450
+ )
451
+ if self.top_bar_area.environment:
452
+ environment = self.environments_repo.get_by_name(
453
+ name=self.top_bar_area.environment
454
+ ).data
455
+ request = request.resolve_variables(environment.variables)
456
+ return request
457
+
372
458
  def set_request(self, request: Request) -> None:
373
459
  self.url_area.method = request.method
374
460
  self.url_area.url = request.url
@@ -437,11 +523,11 @@ class RESTinyApp(App, inherit_bindings=False):
437
523
  self.request_area.option_timeout = str(request.options.timeout)
438
524
 
439
525
  async def _send_request(self) -> None:
440
- self.response_area.set_data(data=None)
526
+ self.response_area.clear()
441
527
  self.response_area.loading = True
442
528
  self.url_area.request_pending = True
443
529
  try:
444
- request = self.get_request()
530
+ request = self.get_resolved_request()
445
531
  async with httpx.AsyncClient(
446
532
  timeout=request.options.timeout,
447
533
  follow_redirects=request.options.follow_redirects,
@@ -460,10 +546,10 @@ class RESTinyApp(App, inherit_bindings=False):
460
546
  self.notify(f'{error_name}: {error_message}', severity='error')
461
547
  else:
462
548
  self.notify(f'{error_name}', severity='error')
463
- self.response_area.set_data(data=None)
549
+ self.response_area.clear()
464
550
  self.response_area.is_showing_response = False
465
551
  except asyncio.CancelledError:
466
- self.response_area.set_data(data=None)
552
+ self.response_area.clear()
467
553
  self.response_area.is_showing_response = False
468
554
  finally:
469
555
  self.response_area.loading = False
@@ -16,6 +16,7 @@ from textual.widgets import (
16
16
  Select,
17
17
  Static,
18
18
  )
19
+ from textual.widgets.tree import TreeNode
19
20
 
20
21
  from restiny.entities import Folder, Request
21
22
  from restiny.enums import HTTPMethod
@@ -170,8 +171,7 @@ class _BaseEditScreen(ModalScreen):
170
171
 
171
172
 
172
173
  class _AddScreen(_BaseEditScreen):
173
- if TYPE_CHECKING:
174
- app: RESTinyApp
174
+ app: 'RESTinyApp'
175
175
 
176
176
  @on(Button.Pressed, '#confirm')
177
177
  def _on_confirm(self, message: Button.Pressed) -> None:
@@ -220,8 +220,7 @@ class _AddScreen(_BaseEditScreen):
220
220
 
221
221
 
222
222
  class _UpdateScreen(_BaseEditScreen):
223
- if TYPE_CHECKING:
224
- app: RESTinyApp
223
+ app: 'RESTinyApp'
225
224
 
226
225
  def __init__(self, id: int, *args, **kwargs) -> None:
227
226
  super().__init__(*args, **kwargs)
@@ -284,8 +283,7 @@ class _UpdateScreen(_BaseEditScreen):
284
283
 
285
284
 
286
285
  class CollectionsArea(Widget):
287
- if TYPE_CHECKING:
288
- app: RESTinyApp
286
+ app: 'RESTinyApp'
289
287
 
290
288
  ALLOW_MAXIMIZE = True
291
289
  focusable = True
@@ -478,6 +476,15 @@ class CollectionsArea(Widget):
478
476
  if not result.confirmed:
479
477
  return
480
478
 
479
+ try:
480
+ prev_selected_index_in_parent = (
481
+ self.collections_tree.cursor_node.parent.children.index(
482
+ self.collections_tree.cursor_node
483
+ )
484
+ )
485
+ except ValueError:
486
+ prev_selected_index_in_parent = 0
487
+
481
488
  if self.collections_tree.cursor_node.allow_expand:
482
489
  self.app.folders_repo.delete_by_id(
483
490
  self.collections_tree.cursor_node.data['id']
@@ -507,11 +514,24 @@ class CollectionsArea(Widget):
507
514
  )
508
515
  )
509
516
 
510
- def _populate_children(self, node) -> None:
511
- folder_id = node.data['id']
517
+ if self.collections_tree.cursor_node.parent.children:
518
+ next_index_to_select = min(
519
+ prev_selected_index_in_parent,
520
+ len(self.collections_tree.cursor_node.parent.children) - 1,
521
+ )
522
+ next_node_to_select = (
523
+ self.collections_tree.cursor_node.parent.children[
524
+ next_index_to_select
525
+ ]
526
+ )
527
+ else:
528
+ next_node_to_select = self.collections_tree.cursor_node.parent
529
+ self.call_after_refresh(
530
+ lambda: self.collections_tree.select_node(next_node_to_select)
531
+ )
512
532
 
513
- for child in list(node.children):
514
- child.remove()
533
+ def _populate_children(self, node: TreeNode) -> None:
534
+ folder_id = node.data['id']
515
535
 
516
536
  folders = self.app.folders_repo.list_by_parent_id(folder_id).data
517
537
  requests = self.app.requests_repo.list_by_folder_id(folder_id).data
@@ -528,6 +548,9 @@ class CollectionsArea(Widget):
528
548
  )
529
549
  sorted_requests = sorted(requests, key=sort_requests)
530
550
 
551
+ for child_node in list(node.children):
552
+ self.collections_tree.remove(child_node)
553
+
531
554
  for folder in sorted_folders:
532
555
  self.collections_tree.add_folder(
533
556
  parent_node=node, name=folder.name, id=folder.id
@@ -541,6 +564,8 @@ class CollectionsArea(Widget):
541
564
  id=request.id,
542
565
  )
543
566
 
567
+ node.refresh()
568
+
544
569
  def _resolve_all_folder_paths(self) -> list[dict[str, str | int | None]]:
545
570
  paths: list[dict[str, str | int | None]] = [{'path': '/', 'id': None}]
546
571