python-fx 0.3.2__py3-none-any.whl → 0.4.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.
pyfx/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.2"
1
+ __version__ = "0.4.0"
pyfx/app.py CHANGED
@@ -10,7 +10,8 @@ from pyfx.config import parse
10
10
  from pyfx.config import themes_path
11
11
  from pyfx.config.config_parser import load
12
12
  from pyfx.error import PyfxException
13
- from pyfx.model import Model
13
+ from pyfx.model.model_manager import ModelManager
14
+ from pyfx.model.model_manager import ModelResult
14
15
  from pyfx.service.client import Client
15
16
  from pyfx.service.dispatcher import Dispatcher
16
17
  from pyfx.view import View
@@ -53,10 +54,12 @@ class PyfxApp:
53
54
 
54
55
  # backend part
55
56
  self._dispatcher = Dispatcher()
56
- # model
57
- self._model = Model(self._data)
58
- self._dispatcher.register("query", self._model.query)
59
- self._dispatcher.register("complete", self._model.complete)
57
+ # model manager
58
+ self._model_manager = ModelManager(
59
+ result_callback=self.__handle_model_result,
60
+ progress_callback=None)
61
+ self._dispatcher.register("query", self._model_manager.query)
62
+ self._dispatcher.register("complete", self._model_manager.complete)
60
63
 
61
64
  # UI part
62
65
  self._keymapper = self.__convert_keymap(self._config.ui.keymap)
@@ -194,9 +197,19 @@ class PyfxApp:
194
197
  "exit with {}", e)
195
198
  finally:
196
199
  self._thread_pool_executor.shutdown(wait=True)
200
+ self._model_manager.shutdown(wait=True)
197
201
  self._screen.clear()
198
202
 
199
- def __init(self):
203
+ def process_input(self, keys):
204
+ """
205
+ Test-used method to process a list of keypress with proper model initialization
206
+ """
207
+ init_success = self.__init(blocking=True)
208
+ if not init_success:
209
+ return False, "Model failed to load within timeout"
210
+ return self.__process_input(keys)
211
+
212
+ def __init(self, blocking=False):
200
213
  """Post-initializes Pyfx, it must be called before `__run()`.
201
214
 
202
215
  .. note::
@@ -207,13 +220,22 @@ class PyfxApp:
207
220
  processing data to construct essential widgets.
208
221
  """
209
222
  logger.debug("Initializing Pyfx...")
210
- self._json_browser.refresh_view(self._data)
223
+ # Start async model loading
224
+ self._model_manager.load(self._data)
225
+ if not blocking:
226
+ return True
227
+ return self._model_manager.wait_until_ready(timeout=5.0)
211
228
 
212
229
  def __run(self):
213
230
  """Starts the UI loop."""
214
231
  logger.debug("Running Pyfx...")
215
232
  self._view.run()
216
233
 
234
+ def __process_input(self, keys):
235
+ """Test-used method to process a list of keypress with proper model initialization."""
236
+ logger.debug("Running Pyfx...")
237
+ return self._view.process_input(keys)
238
+
217
239
  def __init_logger(self, is_debug_mode):
218
240
  logger.configure(
219
241
  handlers=[{
@@ -266,3 +288,14 @@ class PyfxApp:
266
288
  # avoid potential error during e2e test
267
289
  pass
268
290
  return screen
291
+
292
+ def __handle_model_result(self, result: ModelResult):
293
+ """Handle async model results"""
294
+ if not result.success:
295
+ logger.error("Model operation failed: {}", result.error)
296
+ return
297
+
298
+ if result.operation_name == "Load":
299
+ logger.debug("Model loading completed, refreshing view")
300
+ # Use urwid alarm to safely update UI from background thread
301
+ self._mediator.notify('backend', 'refresh', 'json_browser', result.data)
pyfx/model/model.py CHANGED
@@ -13,10 +13,16 @@ class Model:
13
13
  * performs auto-completion with given JSONPath query
14
14
  """
15
15
 
16
- def __init__(self, data):
16
+ def __init__(self):
17
+ self._data = None
18
+ self._current = None
19
+
20
+ def load(self, data):
17
21
  self._data = data
18
22
  self._current = data
19
23
 
24
+ return self._current
25
+
20
26
  def query(self, text):
21
27
  if self._data is None:
22
28
  logger.debug("Data is None.")
@@ -0,0 +1,210 @@
1
+ import threading
2
+ import queue
3
+ from typing import Any, Optional, Callable
4
+ from concurrent.futures import ThreadPoolExecutor, Future
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from loguru import logger
8
+
9
+ from pyfx.model import Model
10
+
11
+
12
+ class ModelState(Enum):
13
+ CREATED = "created"
14
+ LOADING = "loading"
15
+ READY = "ready"
16
+ SHUTDOWN = "shutdown"
17
+
18
+
19
+ @dataclass
20
+ class ModelResult:
21
+ """Result wrapper for threaded model operations"""
22
+
23
+ success: bool
24
+ operation_name: str
25
+ data: Any = None
26
+ error: Optional[str] = None
27
+ operation_id: Optional[str] = None
28
+
29
+
30
+ @dataclass
31
+ class ProgressUpdate:
32
+ """Progress update for long-running operations"""
33
+
34
+ operation_id: str
35
+ progress: float # 0.0 to 1.0
36
+ message: str
37
+ completed: bool = False
38
+
39
+
40
+ class ModelManager:
41
+ """
42
+ Thread-safe wrapper around Model that runs operations in background thread.
43
+
44
+ Provides async interface for JSON processing while maintaining thread safety
45
+ between model operations and UI updates.
46
+ """
47
+
48
+ def __init__(self, result_callback, progress_callback, max_workers: int = 2):
49
+ self._executor = ThreadPoolExecutor(
50
+ max_workers=max_workers, thread_name_prefix="pyfx-model"
51
+ )
52
+
53
+ # Thread-safe communication
54
+ self._result_queue = queue.Queue()
55
+ self._progress_queue = queue.Queue()
56
+ self._operation_counter = 0
57
+ self._operation_lock = threading.Lock()
58
+
59
+ # Callbacks for UI updates
60
+ self._result_callback: Optional[Callable[[ModelResult], None]] = result_callback
61
+ self._progress_callback: Optional[Callable[[ProgressUpdate], None]] = progress_callback
62
+
63
+ # Current operation tracking
64
+ self._current_futures: dict[str, Future] = {}
65
+
66
+ self._state = ModelState.CREATED
67
+ self._model: Model = Model()
68
+
69
+ def load(self, data: Any) -> str:
70
+ """Load data asynchronously"""
71
+
72
+ def _load_task(operation_id, operation_name):
73
+ try:
74
+ self._report_progress(operation_id, 0.0, "Initializing model...")
75
+
76
+ self._state = ModelState.LOADING
77
+ result = self._model.load(data)
78
+
79
+ self._state = ModelState.READY
80
+ self._report_progress(operation_id, 1.0, "Model ready", completed=True)
81
+
82
+ model_result = ModelResult(
83
+ operation_id=operation_id,
84
+ operation_name=operation_name,
85
+ success=True,
86
+ data=result,
87
+ )
88
+ self._report_result(model_result)
89
+ except Exception as e:
90
+ logger.opt(exception=True).error("Load task failed: {}", e)
91
+ self._state = ModelState.SHUTDOWN
92
+ raise
93
+
94
+ return self._submit_task("Load", _load_task)
95
+
96
+ def query(self, text: str):
97
+ """Execute JSONPath query synchronously"""
98
+ if self._state != ModelState.READY:
99
+ logger.warning(
100
+ "Model not ready for queries, current state: {}", self._state
101
+ )
102
+ return None
103
+
104
+ return self._model.query(text)
105
+
106
+ def complete(self, text: str):
107
+ """Execute autocompletion synchronously"""
108
+ if self._state != ModelState.READY:
109
+ logger.warning(
110
+ "Model not ready for completion, current state: {}", self._state
111
+ )
112
+ return False, "", []
113
+
114
+ return self._model.complete(text)
115
+
116
+ def cancel_operation(self, task_id: str) -> bool:
117
+ """Cancel a running operation"""
118
+ if task_id not in self._current_futures:
119
+ return False
120
+
121
+ future = self._current_futures[task_id]
122
+ if not future.cancel():
123
+ return False
124
+
125
+ del self._current_futures[task_id]
126
+ return True
127
+
128
+ def wait_until_ready(self, timeout: float = 10.0) -> bool:
129
+ """Block until model is ready or timeout occurs"""
130
+ import time
131
+ start_time = time.time()
132
+
133
+ while self._state != ModelState.READY:
134
+ if time.time() - start_time > timeout:
135
+ logger.warning("Model loading timeout after {}s", timeout)
136
+ return False
137
+
138
+ time.sleep(0.01) # Small sleep to avoid busy waiting
139
+
140
+ return True
141
+
142
+ def shutdown(self, wait: bool = True):
143
+ """Shutdown the model manager"""
144
+ self._state = ModelState.SHUTDOWN
145
+
146
+ # Cancel all pending operations
147
+ for operation_id in list(self._current_futures.keys()):
148
+ self.cancel_operation(operation_id)
149
+
150
+ # Shutdown executor
151
+ self._executor.shutdown(wait=wait)
152
+
153
+ def _submit_task(self, task_name, task):
154
+ task_id = self.__generate_task_id()
155
+
156
+ def wrapped_task():
157
+ try:
158
+ task(task_id, task_name)
159
+ except Exception as e:
160
+ logger.opt(exception=True).error("{} failed: {}", task_name, e)
161
+ self._report_error(
162
+ task_id, task_name, f"{task_name} failed: {str(e)}"
163
+ )
164
+
165
+ future = self._executor.submit(wrapped_task)
166
+ self._current_futures[task_id] = future
167
+ return task_id
168
+
169
+ def __generate_task_id(self) -> str:
170
+ """Generate unique operation ID"""
171
+ with self._operation_lock:
172
+ self._operation_counter += 1
173
+ return f"op_{self._operation_counter}"
174
+
175
+ def _report_progress(
176
+ self, operation_id: str, progress: float, message: str, completed: bool = False
177
+ ):
178
+ """Report progress update"""
179
+ update = ProgressUpdate(
180
+ operation_id=operation_id,
181
+ progress=progress,
182
+ message=message,
183
+ completed=completed,
184
+ )
185
+
186
+ try:
187
+ self._progress_queue.put_nowait(update)
188
+ if self._progress_callback:
189
+ self._progress_callback(update)
190
+ except queue.Full:
191
+ logger.warning("Progress queue full, dropping update")
192
+
193
+ def _report_result(self, result: ModelResult):
194
+ """Report operation result"""
195
+ try:
196
+ self._result_queue.put_nowait(result)
197
+ if self._result_callback:
198
+ self._result_callback(result)
199
+ except queue.Full:
200
+ logger.warning("Result queue full, dropping result")
201
+
202
+ def _report_error(self, operation_id: str, operation_name: str, error_msg: str):
203
+ """Report operation error"""
204
+ result = ModelResult(
205
+ operation_id=operation_id,
206
+ success=False,
207
+ operation_name=operation_name,
208
+ error=error_msg,
209
+ )
210
+ self._report_result(result)
pyfx/view/common/frame.py CHANGED
@@ -62,7 +62,7 @@ class Frame(urwid.Widget, urwid.WidgetContainerMixin):
62
62
  `current_mini_buffer`(string): the key of current focused widget
63
63
  for mini buffer in mini buffers.
64
64
  """
65
- self.__super.__init__()
65
+ super().__init__()
66
66
 
67
67
  self._screen = screen
68
68
 
pyfx/view/view_manager.py CHANGED
@@ -48,11 +48,9 @@ class View:
48
48
  for index, key in enumerate(keys):
49
49
  # work around for urwid.MainLoop#process_input does not apply
50
50
  # input filter
51
- key = self._loop.input_filter([key], None)
51
+ key = self._loop.input_filter([key], [])
52
52
 
53
- if len(key) == 0:
54
- continue
55
- elif self._loop.process_input(key):
53
+ if len(key) == 0 or self._loop.process_input(key):
56
54
  continue
57
55
 
58
56
  return False, f"keys[{index}]: {key} is not handled"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-fx
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A python-native fx-alike terminal JSON viewer.
5
5
  Author-email: Yutian Wu <yutianwu@umich.edu>
6
6
  License: MIT
@@ -13,32 +13,32 @@ Classifier: Operating System :: POSIX
13
13
  Classifier: Operating System :: MacOS :: MacOS X
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Topic :: Utilities
16
- Requires-Python: >=3.8
16
+ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE.txt
19
- Requires-Dist: antlr4-python3-runtime==4.13.2
20
- Requires-Dist: first==2.0.2
21
- Requires-Dist: jsonpath-ng==1.6.1
22
- Requires-Dist: ply==3.11
23
- Requires-Dist: pyperclip==1.9.0
24
- Requires-Dist: wcwidth==0.2.13
25
- Requires-Dist: loguru==0.7.2; python_version >= "3.5"
26
- Requires-Dist: dacite==1.8.1; python_version >= "3.6"
27
- Requires-Dist: overrides==7.7.0; python_version >= "3.6"
28
- Requires-Dist: click==8.1.7; python_version >= "3.7"
29
- Requires-Dist: asciimatics==1.15.0; python_version >= "3.8"
30
- Requires-Dist: pillow==10.4.0; python_version >= "3.8"
31
- Requires-Dist: pyyaml==6.0.2; python_version >= "3.8"
32
- Requires-Dist: typing-extensions==4.12.2; python_version >= "3.8"
33
- Requires-Dist: urwid==2.6.15; python_version >= "3.8"
34
- Requires-Dist: yamale==5.2.1; python_version >= "3.8"
35
- Requires-Dist: pyfiglet==1.0.2; python_version >= "3.9"
19
+ Requires-Dist: antlr4-python3-runtime<5,>=4.13
20
+ Requires-Dist: asciimatics<2,>=1.15
21
+ Requires-Dist: click<9,>=8.1.7
22
+ Requires-Dist: first<3,==2.0
23
+ Requires-Dist: dacite<2,==1.8
24
+ Requires-Dist: jsonpath-ng<2,==1.6
25
+ Requires-Dist: loguru<0.8,>=0.7.2
26
+ Requires-Dist: overrides<8,>=7.7.0
27
+ Requires-Dist: pillow<11,>=10.4
28
+ Requires-Dist: ply<4,>=3.11
29
+ Requires-Dist: pyfiglet<2,>=1.0
30
+ Requires-Dist: pyperclip>=1.9
31
+ Requires-Dist: pyyaml<7,>=6.0.2
32
+ Requires-Dist: typing-extensions<5,>=4.12.2
33
+ Requires-Dist: urwid<3,>=2.6
34
+ Requires-Dist: wcwidth<0.3,>=0.2.13
35
+ Requires-Dist: yamale<6,>=5.2
36
+ Dynamic: license-file
36
37
 
37
38
  # Pyfx
38
39
  ![Build Status](https://github.com/cielong/pyfx/actions/workflows/ci.yml/badge.svg?branch=main)
39
40
  ![Documentation Status](https://readthedocs.org/projects/python-fx/badge/?version=latest)
40
41
  ![PyPI version](https://badge.fury.io/py/python-fx.svg)
41
- ![Python](https://img.shields.io/badge/python-3.8-green.svg)
42
42
  ![Python](https://img.shields.io/badge/python-3.9-green.svg)
43
43
  ![Python](https://img.shields.io/badge/python-3.10-green.svg)
44
44
  ![Python](https://img.shields.io/badge/python-3.11-green.svg)
@@ -69,7 +69,7 @@ A python-native JSON Viewer TUI, inspired by [fx](https://github.com/antonmedv/f
69
69
 
70
70
  ## Prerequisites
71
71
  * OS: MacOS / Linux
72
- * python: >= 3.8
72
+ * python: >= 3.9
73
73
  * pip
74
74
 
75
75
  ## Installation
@@ -1,6 +1,6 @@
1
1
  pyfx/__init__.py,sha256=KU0-7SWHjuFrvQ3UJHeUcUG41-IpwGBSCjIibDD52H8,25
2
- pyfx/__version__.py,sha256=vNiWJ14r_cw5t_7UDqDQIVZvladKFGyHH2avsLpN7Vg,22
3
- pyfx/app.py,sha256=DyDjKTPZszHEEABMu02DrU2n6FUpl9VJwwNeL7bpivk,10699
2
+ pyfx/__version__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
3
+ pyfx/app.py,sha256=WwpjxneQY0lBf6tI9l9HdA2LjUmsLyav9f9tfmX__L4,12143
4
4
  pyfx/cli.py,sha256=9mi8kOiZFcY4RCN1TUV5aCeKmuoDwZm-1kVRdF7vbNE,2135
5
5
  pyfx/cli_utils.py,sha256=TLF-242Twi54i8URozwxmZVfEibvmwDWDE5AFyWqC58,1243
6
6
  pyfx/error.py,sha256=9AQ7q3zJQJEvvWGv_39Ep17KoN3eSiRgeqhh4_-7Mqg,96
@@ -17,7 +17,8 @@ pyfx/config/yaml/keymaps/vim.yml,sha256=sNW6AsUXHxUNrF9u_EXXlU92WYbay1X94XfcA4ZK
17
17
  pyfx/config/yaml/themes/__init__.py,sha256=CY_rp0Unst3ZQ9npKe6cxdwrs9yrbsMdeQGH-q2W7iM,30
18
18
  pyfx/config/yaml/themes/basic.yml,sha256=lffeUU4ABZ57-eL7-tXTCuXqtqrXDv12V_Qk0AmhpxQ,1217
19
19
  pyfx/model/__init__.py,sha256=TrL7n4E7ZA7EmNhk6Pp-YtMPxOl90vyEd9jymd143eI,246
20
- pyfx/model/model.py,sha256=8lh9tf-67nPTruDmPRkGHVt2y9Ufz1fP0OJ0ciwut00,1224
20
+ pyfx/model/model.py,sha256=-pIOgdP32dTA3mzjSVRlmLoyhvRQbISa5FGlrmus1nU,1330
21
+ pyfx/model/model_manager.py,sha256=YmKXLxsj300_u3CEwH98KjX725zToUKxteX8CyiFNj4,6614
21
22
  pyfx/model/autocomplete/__init__.py,sha256=PPKRsvPcvJQcJL-Y74BEs0_26haxlnvnBxa_sqbXF6U,517
22
23
  pyfx/model/autocomplete/autocomplete_listener.py,sha256=SrQ5WlMfSVwcjOPf08_bZUqNya5hX3JNefkYB0SNbqk,11667
23
24
  pyfx/model/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -37,10 +38,10 @@ pyfx/view/__init__.py,sha256=XZG19brgFnZcARuPifl9PqwNXgPc3drJHq6NwNjol3s,198
37
38
  pyfx/view/keys.py,sha256=GgW1yX68OscZr_WPzzc8MnsUmFo7TKbV4BYvI78FgTc,3242
38
39
  pyfx/view/themes.py,sha256=mXo2_ywvMxAedj8a2LzbGIyP2mWt8zZSjvfqrYqWEhk,1840
39
40
  pyfx/view/view_frame.py,sha256=zdSgN_CYGmxp2PgWM5q8qPOGVWcprmy6-ugsbswX-yQ,2476
40
- pyfx/view/view_manager.py,sha256=wjVegAkMpw6cbLs2Alp2a37KCeUzUmh_rKku-7juRf0,1856
41
+ pyfx/view/view_manager.py,sha256=-jftt-aoKBjqTyw41X0XQYNZJNrIbBP36VwNFI14nr8,1806
41
42
  pyfx/view/view_mediator.py,sha256=mY1-IT77Xi7DwtLW-hyrSE4NdFCPNXffnaFHztnL53s,1137
42
43
  pyfx/view/common/__init__.py,sha256=yD8y941cJ_fR8XYbSmKzOIAUhRt3eSA60pU1PRYfyZY,137
43
- pyfx/view/common/frame.py,sha256=TM-YNoqsjEuXkADPJDI5TjZ6o-g8LYvMAfDDoAhPN6A,11601
44
+ pyfx/view/common/frame.py,sha256=7IjKe6-BOc6CGlj6lL2EfC5z7bSnGhSTVtc1DxhISRk,11596
44
45
  pyfx/view/common/popup.py,sha256=P6MyMMpR_Mvqz7SvsTQN-wjIe2DgFeomSy4j4X3s0bU,1835
45
46
  pyfx/view/common/selectable_text.py,sha256=vg70CrO8495RztSsdw45JDgtH5zBQAO7DesHSiQ9yqg,490
46
47
  pyfx/view/components/__init__.py,sha256=W1jwydHqN8Yfbh46sujZnNML2Pfs7opfNe0edADwh5U,352
@@ -78,9 +79,9 @@ pyfx/view/json_lib/primitive/integer.py,sha256=C-xOeR3deeVobAkvEoIfGYFu0koNIMsGk
78
79
  pyfx/view/json_lib/primitive/null.py,sha256=BL9CuFTGmHt6tmXjIvuOwxIEwpesp0Df4J88aMj8rgM,775
79
80
  pyfx/view/json_lib/primitive/numeric.py,sha256=DYI8eZULo2gKSeyzDtnXk4fgtgBYyoVH_ARGaEJ5YXI,868
80
81
  pyfx/view/json_lib/primitive/string.py,sha256=oBFzZKhrKoKLLdiVg6ivkryB2cJDDs34B-xGXf63F9E,839
81
- python_fx-0.3.2.dist-info/LICENSE.txt,sha256=KxdNyEebr0IyALPixRsvD-2puJr9WxSePEe1UsjxPlA,1066
82
- python_fx-0.3.2.dist-info/METADATA,sha256=g4uTV1GZ6-TdexhWYOfTyIv_X_rH6t8tWjX7_nmK7kA,10342
83
- python_fx-0.3.2.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
84
- python_fx-0.3.2.dist-info/entry_points.txt,sha256=8Veumj2a-mijrFP-07Rph2IRrR78XjTLLFc_gD1U-mE,39
85
- python_fx-0.3.2.dist-info/top_level.txt,sha256=X4zR7lGFzBoSu_P2ocvTC0t50kHygVjqbMA4twR1WZ0,5
86
- python_fx-0.3.2.dist-info/RECORD,,
82
+ python_fx-0.4.0.dist-info/licenses/LICENSE.txt,sha256=KxdNyEebr0IyALPixRsvD-2puJr9WxSePEe1UsjxPlA,1066
83
+ python_fx-0.4.0.dist-info/METADATA,sha256=tCtrBPSiRQ6ni1ne87Gi0kb9LYmn5aSGMcLH4q9Cry8,10060
84
+ python_fx-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
85
+ python_fx-0.4.0.dist-info/entry_points.txt,sha256=8Veumj2a-mijrFP-07Rph2IRrR78XjTLLFc_gD1U-mE,39
86
+ python_fx-0.4.0.dist-info/top_level.txt,sha256=X4zR7lGFzBoSu_P2ocvTC0t50kHygVjqbMA4twR1WZ0,5
87
+ python_fx-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5