python-fx 0.3.2__tar.gz → 0.4.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.
Files changed (111) hide show
  1. {python_fx-0.3.2 → python_fx-0.4.0}/CHANGELOG.md +3 -0
  2. python_fx-0.4.0/CLAUDE.md +121 -0
  3. {python_fx-0.3.2 → python_fx-0.4.0}/PKG-INFO +22 -22
  4. {python_fx-0.3.2 → python_fx-0.4.0}/README.md +1 -2
  5. python_fx-0.4.0/dev-requirements.txt +81 -0
  6. {python_fx-0.3.2 → python_fx-0.4.0}/pyproject.toml +21 -2
  7. python_fx-0.4.0/requirements.txt +15 -0
  8. python_fx-0.4.0/src/pyfx/__version__.py +1 -0
  9. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/app.py +40 -7
  10. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/model.py +7 -1
  11. python_fx-0.4.0/src/pyfx/model/model_manager.py +210 -0
  12. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/frame.py +1 -1
  13. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/view_manager.py +2 -4
  14. {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/PKG-INFO +22 -22
  15. {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/SOURCES.txt +2 -0
  16. python_fx-0.4.0/src/python_fx.egg-info/requires.txt +17 -0
  17. {python_fx-0.3.2 → python_fx-0.4.0}/tox.ini +5 -4
  18. python_fx-0.3.2/dev-requirements.txt +0 -80
  19. python_fx-0.3.2/requirements.txt +0 -17
  20. python_fx-0.3.2/src/pyfx/__version__.py +0 -1
  21. python_fx-0.3.2/src/python_fx.egg-info/requires.txt +0 -27
  22. {python_fx-0.3.2 → python_fx-0.4.0}/.coveragerc +0 -0
  23. {python_fx-0.3.2 → python_fx-0.4.0}/.readthedocs.yml +0 -0
  24. {python_fx-0.3.2 → python_fx-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  25. {python_fx-0.3.2 → python_fx-0.4.0}/LICENSE.txt +0 -0
  26. {python_fx-0.3.2 → python_fx-0.4.0}/MANIFEST.in +0 -0
  27. {python_fx-0.3.2 → python_fx-0.4.0}/docs/Makefile +0 -0
  28. {python_fx-0.3.2 → python_fx-0.4.0}/docs/conf.py +0 -0
  29. {python_fx-0.3.2 → python_fx-0.4.0}/docs/demo.gif +0 -0
  30. {python_fx-0.3.2 → python_fx-0.4.0}/docs/index.rst +0 -0
  31. {python_fx-0.3.2 → python_fx-0.4.0}/docs/make.bat +0 -0
  32. {python_fx-0.3.2 → python_fx-0.4.0}/setup.cfg +0 -0
  33. {python_fx-0.3.2 → python_fx-0.4.0}/setup.py +0 -0
  34. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/__init__.py +0 -0
  35. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/cli.py +0 -0
  36. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/cli_utils.py +0 -0
  37. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/__init__.py +0 -0
  38. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/config.py +0 -0
  39. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/config_parser.py +0 -0
  40. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/__init__.py +0 -0
  41. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/config.yml +0 -0
  42. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/config_schema.yml +0 -0
  43. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/__init__.py +0 -0
  44. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/basic.yml +0 -0
  45. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/emacs.yml +0 -0
  46. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/vim.yml +0 -0
  47. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/themes/__init__.py +0 -0
  48. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/themes/basic.yml +0 -0
  49. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/error.py +0 -0
  50. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/__init__.py +0 -0
  51. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/autocomplete/__init__.py +0 -0
  52. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/autocomplete/autocomplete_listener.py +0 -0
  53. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/__init__.py +0 -0
  54. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPath.g4 +0 -0
  55. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPath.interp +0 -0
  56. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPath.tokens +0 -0
  57. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathLexer.interp +0 -0
  58. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathLexer.py +0 -0
  59. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathLexer.tokens +0 -0
  60. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathListener.py +0 -0
  61. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathParser.py +0 -0
  62. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/__init__.py +0 -0
  63. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/service/__init__.py +0 -0
  64. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/service/client.py +0 -0
  65. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/service/dispatcher.py +0 -0
  66. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/__init__.py +0 -0
  67. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/__init__.py +0 -0
  68. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/popup.py +0 -0
  69. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/selectable_text.py +0 -0
  70. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/__init__.py +0 -0
  71. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/abstract_component_keys.py +0 -0
  72. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/autocomplete_popup.py +0 -0
  73. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/help_popup.py +0 -0
  74. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/json_browser.py +0 -0
  75. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/query_bar.py +0 -0
  76. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/warning_bar.py +0 -0
  77. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/__init__.py +0 -0
  78. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/__init__.py +0 -0
  79. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_end_node.py +0 -0
  80. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_end_widget.py +0 -0
  81. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_node.py +0 -0
  82. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_start_widget.py +0 -0
  83. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_unexpanded_widget.py +0 -0
  84. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_composite_end_node.py +0 -0
  85. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_composite_node.py +0 -0
  86. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_listbox.py +0 -0
  87. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_listwalker.py +0 -0
  88. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_node_creator.py +0 -0
  89. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_node_factory.py +0 -0
  90. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_simple_node.py +0 -0
  91. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_widget.py +0 -0
  92. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/__init__.py +0 -0
  93. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_end_node.py +0 -0
  94. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_end_widget.py +0 -0
  95. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_node.py +0 -0
  96. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_start_widget.py +0 -0
  97. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_unexpanded_widget.py +0 -0
  98. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/__init__.py +0 -0
  99. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/base.py +0 -0
  100. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/boolean.py +0 -0
  101. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/integer.py +0 -0
  102. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/null.py +0 -0
  103. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/numeric.py +0 -0
  104. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/string.py +0 -0
  105. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/keys.py +0 -0
  106. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/themes.py +0 -0
  107. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/view_frame.py +0 -0
  108. {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/view_mediator.py +0 -0
  109. {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/dependency_links.txt +0 -0
  110. {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/entry_points.txt +0 -0
  111. {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/top_level.txt +0 -0
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
+ ### Changed
10
+ - Allow dynamic versions for dependency
11
+ - Remove support for python 3.8
9
12
 
10
13
  ## [0.3.2] - 2024-08-12
11
14
  ### Changed
@@ -0,0 +1,121 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Pyfx is a Python-native JSON viewer TUI (Terminal User Interface) inspired by fx. It provides an interactive terminal-based interface for browsing and querying JSON data with JSONPath support.
8
+
9
+ ## Development Commands
10
+
11
+ ### Core Development Workflow
12
+ - `make install` - Full build pipeline: clean, lint, test, build, and install
13
+ - `make build` - Build the project (includes clean, lint, test)
14
+ - `make test` - Run all tests via tox (includes clean, lint)
15
+ - `make lint` - Format code with autopep8
16
+ - `make clean` - Remove build artifacts
17
+
18
+ ### Testing
19
+ - `tox` - Run tests across multiple Python versions (3.8-3.11)
20
+ - `tox -e py311` - Run tests for specific Python version
21
+ - `tox -e style_check` - Run flake8 style checks
22
+ - `pytest` - Run tests directly (after installing dev dependencies)
23
+ - `pytest --cov=pyfx --cov-report=term-missing` - Run tests with coverage
24
+
25
+ ### Dependency Management
26
+ - `pipenv install --dev` - Install development dependencies
27
+ - `pipenv requirements > requirements.txt` - Update requirements.txt
28
+ - `make lock` - Generate both requirements.txt and dev-requirements.txt
29
+
30
+ ### Installation and Running
31
+ - `pip install python-fx` - Install from PyPI
32
+ - `pyfx data.json` - Open JSON file
33
+ - `cat data.json | pyfx` - Read JSON from pipe
34
+ - `pyfx -x` - Read JSON from clipboard
35
+ - `pyfx --debug` - Enable debug logging
36
+
37
+ ## Architecture
38
+
39
+ ### High-Level Architecture
40
+ Pyfx follows an MVC-like pattern with additional service and mediator layers:
41
+
42
+ - **Model** (`src/pyfx/model/`) - Data processing and JSONPath queries
43
+ - **View** (`src/pyfx/view/`) - UI components built on urwid
44
+ - **Service** (`src/pyfx/service/`) - Business logic dispatcher and client
45
+ - **Config** (`src/pyfx/config/`) - Configuration management (themes, keymaps)
46
+
47
+ ### Key Components
48
+
49
+ #### App Layer (`app.py`)
50
+ - `PyfxApp` - Main application entry point and dependency injection container
51
+ - Orchestrates all components and manages the UI lifecycle
52
+
53
+ #### Model Layer
54
+ - `Model` - Handles JSONPath queries and autocomplete functionality
55
+ - `autocomplete/` - JSONPath query autocompletion logic
56
+
57
+ #### View Layer
58
+ - **Core UI Framework**:
59
+ - `ViewFrame` - Main window frame management
60
+ - `ViewMediator` - Component communication mediator
61
+ - `View` - urwid MainLoop wrapper
62
+
63
+ - **Components** (`view/components/`):
64
+ - `JSONBrowser` - Main JSON tree viewer
65
+ - `QueryBar` - JSONPath query input
66
+ - `AutoCompletePopUp` - Query autocompletion popup
67
+ - `HelpPopUp` - Help system
68
+ - `WarningBar` - Error/warning display
69
+
70
+ - **JSON Rendering** (`view/json_lib/`):
71
+ - `JSONNodeFactory` - Creates appropriate widgets for JSON types
72
+ - `array/`, `object/`, `primitive/` - Type-specific rendering logic
73
+ - `JSONListBox`, `JSONListWalker` - Core list display widgets
74
+
75
+ #### Service Layer
76
+ - `Dispatcher` - Message routing between components
77
+ - `Client` - Async operation handling with ThreadPoolExecutor
78
+
79
+ #### Configuration System
80
+ - YAML-based configuration for themes and keymaps
81
+ - Predefined configurations: `basic`, `emacs`, `vim` keymaps
82
+ - Theme system with customizable colors for JSON types
83
+
84
+ ### Data Flow
85
+ 1. CLI loads JSON data from file/pipe/clipboard
86
+ 2. `PyfxApp` initializes all components with dependency injection
87
+ 3. `Model` processes JSONPath queries against data
88
+ 4. `ViewMediator` coordinates communication between UI components
89
+ 5. `JSONBrowser` renders JSON using type-specific widgets from `json_lib`
90
+ 6. User interactions trigger events through `KeyMapper` and `Dispatcher`
91
+
92
+ ### Key Design Patterns
93
+ - **Mediator Pattern**: `ViewMediator` decouples component communication
94
+ - **Factory Pattern**: `JSONNodeFactory` creates appropriate widgets
95
+ - **Command Pattern**: `Dispatcher` handles service requests
96
+ - **Observer Pattern**: Components register with mediator for updates
97
+
98
+ ## Development Notes
99
+
100
+ ### ANTLR Grammar
101
+ - JSONPath parsing uses ANTLR v4 grammar (`model/common/jsonpath/JSONPath.g4`)
102
+ - Generated parsers are committed to avoid build dependencies
103
+ - ANTLR version must match Python runtime version
104
+
105
+ ### Testing Structure
106
+ - `tests/unit_tests/` - Component-level unit tests
107
+ - `tests/intergration_tests/` - Integration tests for UI interactions
108
+ - `tests/e2e_tests/` - End-to-end CLI testing
109
+ - `tests/fixtures/` - Test data and configuration files
110
+
111
+ ### Urwid Framework
112
+ - Built on urwid for terminal UI
113
+ - Custom widgets extend urwid base classes
114
+ - Keyboard input handled through urwid's event system
115
+ - Screen management for proper terminal control
116
+
117
+ ### Configuration
118
+ - Default config location: `~/.config/pyfx/config.yml`
119
+ - Fallback to: `src/pyfx/config/yaml/config.yml`
120
+ - Supports custom themes and keymaps
121
+ - YAML schema validation with yamale
@@ -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: asciimatics==1.15.0; python_version >= "3.8"
21
- Requires-Dist: click==8.1.7; python_version >= "3.7"
22
- Requires-Dist: dacite==1.8.1; python_version >= "3.6"
23
- Requires-Dist: first==2.0.2
24
- Requires-Dist: jsonpath-ng==1.6.1
25
- Requires-Dist: loguru==0.7.2; python_version >= "3.5"
26
- Requires-Dist: overrides==7.7.0; python_version >= "3.6"
27
- Requires-Dist: pillow==10.4.0; python_version >= "3.8"
28
- Requires-Dist: ply==3.11
29
- Requires-Dist: pyfiglet==1.0.2; python_version >= "3.9"
30
- Requires-Dist: pyperclip==1.9.0
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: wcwidth==0.2.13
35
- Requires-Dist: yamale==5.2.1; python_version >= "3.8"
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
@@ -2,7 +2,6 @@
2
2
  ![Build Status](https://github.com/cielong/pyfx/actions/workflows/ci.yml/badge.svg?branch=main)
3
3
  ![Documentation Status](https://readthedocs.org/projects/python-fx/badge/?version=latest)
4
4
  ![PyPI version](https://badge.fury.io/py/python-fx.svg)
5
- ![Python](https://img.shields.io/badge/python-3.8-green.svg)
6
5
  ![Python](https://img.shields.io/badge/python-3.9-green.svg)
7
6
  ![Python](https://img.shields.io/badge/python-3.10-green.svg)
8
7
  ![Python](https://img.shields.io/badge/python-3.11-green.svg)
@@ -33,7 +32,7 @@ A python-native JSON Viewer TUI, inspired by [fx](https://github.com/antonmedv/f
33
32
 
34
33
  ## Prerequisites
35
34
  * OS: MacOS / Linux
36
- * python: >= 3.8
35
+ * python: >= 3.9
37
36
  * pip
38
37
 
39
38
  ## Installation
@@ -0,0 +1,81 @@
1
+ alabaster==1.0.0; python_version >= '3.10'
2
+ autopep8==2.3.1; python_version >= '3.8'
3
+ babel==2.18.0; python_version >= '3.8'
4
+ backports.tarfile==1.2.0; python_version >= '3.8'
5
+ build==1.4.0; python_version >= '3.9'
6
+ cachetools==7.0.5; python_version >= '3.10'
7
+ certifi==2026.2.25; python_version >= '3.7'
8
+ charset-normalizer==3.4.6; python_version >= '3.7'
9
+ click==8.3.1; python_version >= '3.10'
10
+ colorama==0.4.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
11
+ coverage[toml]==7.13.5; python_version >= '3.10'
12
+ distlib==0.4.0
13
+ docutils==0.22.4; python_version >= '3.9'
14
+ filelock==3.25.2; python_version >= '3.10'
15
+ flake8==7.1.1; python_full_version >= '3.8.1'
16
+ id==1.6.1; python_version >= '3.9'
17
+ idna==3.11; python_version >= '3.8'
18
+ imagesize==2.0.0; python_version >= '3.10' and python_version < '3.15'
19
+ importlib-metadata==8.7.1; python_version >= '3.9'
20
+ iniconfig==2.3.0; python_version >= '3.10'
21
+ jaraco.classes==3.4.0; python_version >= '3.8'
22
+ jaraco.context==6.1.1; python_version >= '3.9'
23
+ jaraco.functools==4.4.0; python_version >= '3.9'
24
+ jinja2==3.1.6; python_version >= '3.7'
25
+ keyring==25.7.0; python_version >= '3.9'
26
+ markdown-it-py==4.0.0; python_version >= '3.10'
27
+ markupsafe==3.0.3; python_version >= '3.9'
28
+ mccabe==0.7.0; python_version >= '3.6'
29
+ mdurl==0.1.2; python_version >= '3.7'
30
+ more-itertools==10.8.0; python_version >= '3.9'
31
+ nh3==0.3.3; python_version >= '3.8'
32
+ packaging==26.0; python_version >= '3.8'
33
+ parameterized==0.9.0; python_version >= '3.7'
34
+ pillow==12.1.1; python_version >= '3.10'
35
+ platformdirs==4.9.4; python_version >= '3.10'
36
+ pluggy==1.6.0; python_version >= '3.9'
37
+ pycodestyle==2.12.1; python_version >= '3.8'
38
+ pyflakes==3.2.0; python_version >= '3.8'
39
+ pygments==2.19.2; python_version >= '3.8'
40
+ pyproject-api==1.10.0; python_version >= '3.10'
41
+ pyproject-hooks==1.2.0; python_version >= '3.7'
42
+ pytest==9.0.2; python_version >= '3.10'
43
+ pytest-cov==7.0.0; python_version >= '3.9'
44
+ python-discovery==1.1.3; python_version >= '3.8'
45
+ readme-renderer==44.0; python_version >= '3.9'
46
+ requests==2.32.5; python_version >= '3.9'
47
+ requests-toolbelt==1.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
48
+ rfc3986==2.0.0; python_version >= '3.7'
49
+ rich==14.3.3; python_full_version >= '3.8.0'
50
+ roman-numerals==4.1.0; python_version >= '3.10'
51
+ setuptools==82.0.1; python_version >= '3.9'
52
+ snowballstemmer==3.0.1; python_version not in '3.0, 3.1, 3.2'
53
+ sphinx==9.0.4; python_version >= '3.11'
54
+ sphinx-click==6.2.0; python_version >= '3.10'
55
+ sphinxcontrib-applehelp==2.0.0; python_version >= '3.9'
56
+ sphinxcontrib-devhelp==2.0.0; python_version >= '3.9'
57
+ sphinxcontrib-htmlhelp==2.1.0; python_version >= '3.9'
58
+ sphinxcontrib-jsmath==1.0.1; python_version >= '3.5'
59
+ sphinxcontrib-qthelp==2.0.0; python_version >= '3.9'
60
+ sphinxcontrib-serializinghtml==2.0.0; python_version >= '3.9'
61
+ tomli==2.0.1; python_version >= '3.7'
62
+ tomli-w==1.2.0; python_version >= '3.9'
63
+ tox==4.50.0; python_version >= '3.10'
64
+ tox-gh-actions==3.5.0; python_version >= '3.7'
65
+ twine==6.2.0; python_version >= '3.9'
66
+ urllib3==2.6.3; python_version >= '3.9'
67
+ virtualenv==21.2.0; python_version >= '3.8'
68
+ zipp==3.23.0; python_version >= '3.9'
69
+ antlr4-python3-runtime==4.13.2
70
+ asciimatics==1.15.0; python_version >= '3.8'
71
+ dacite==1.9.2; python_version >= '3.7'
72
+ first==2.0.2
73
+ jsonpath-ng==1.8.0
74
+ loguru==0.7.3; python_version >= '3.5' and python_version < '4.0'
75
+ overrides==7.7.0; python_version >= '3.6'
76
+ pyfiglet==1.0.4; python_version >= '3.9'
77
+ pyperclip==1.11.0
78
+ pyyaml==6.0.3; python_version >= '3.8'
79
+ urwid==3.0.5; python_full_version >= '3.9.0'
80
+ wcwidth==0.6.0; python_version >= '3.8'
81
+ yamale==6.1.0; python_version >= '3.8'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-fx"
3
- dynamic = ["version", "dependencies"]
3
+ dynamic = ["version"]
4
4
  authors = [
5
5
  { name = "Yutian Wu", email = "yutianwu@umich.edu" }
6
6
  ]
@@ -8,7 +8,7 @@ license = { text = "MIT" }
8
8
  readme = "README.md"
9
9
  description = "A python-native fx-alike terminal JSON viewer."
10
10
  keywords = ["fx", "pyfx", "json viewer", "tui"]
11
- requires-python = ">=3.8"
11
+ requires-python = ">=3.9"
12
12
  classifiers = [
13
13
  "Development Status :: 4 - Beta",
14
14
  "Programming Language :: Python :: 3 :: Only",
@@ -18,6 +18,25 @@ classifiers = [
18
18
  "License :: OSI Approved :: MIT License",
19
19
  "Topic :: Utilities"
20
20
  ]
21
+ dependencies = [
22
+ "antlr4-python3-runtime>=4.13,<5",
23
+ "asciimatics>=1.15,<2",
24
+ "click>=8.1.7,<9",
25
+ "first==2.0,<3",
26
+ "dacite==1.8,<2",
27
+ "jsonpath-ng==1.6,<2",
28
+ "loguru>=0.7.2,<0.8",
29
+ "overrides>=7.7.0,<8",
30
+ "pillow>=10.4,<11",
31
+ "ply>=3.11,<4",
32
+ "pyfiglet>=1.0,<2",
33
+ "pyperclip>=1.9",
34
+ "pyyaml>=6.0.2,<7",
35
+ "typing-extensions>=4.12.2,<5",
36
+ "urwid>=2.6,<3",
37
+ "wcwidth>=0.2.13,<0.3",
38
+ "yamale>=5.2,<6",
39
+ ]
21
40
 
22
41
  [project.urls]
23
42
  repository = "https://github.com/cielong/pyfx"
@@ -0,0 +1,15 @@
1
+ antlr4-python3-runtime==4.13.2
2
+ asciimatics==1.15.0; python_version >= '3.8'
3
+ click==8.3.1; python_version >= '3.10'
4
+ dacite==1.9.2; python_version >= '3.7'
5
+ first==2.0.2
6
+ jsonpath-ng==1.8.0
7
+ loguru==0.7.3; python_version >= '3.5' and python_version < '4.0'
8
+ overrides==7.7.0; python_version >= '3.6'
9
+ pillow==12.1.1; python_version >= '3.10'
10
+ pyfiglet==1.0.4; python_version >= '3.9'
11
+ pyperclip==1.11.0
12
+ pyyaml==6.0.3; python_version >= '3.8'
13
+ urwid==3.0.5; python_full_version >= '3.9.0'
14
+ wcwidth==0.6.0; python_version >= '3.8'
15
+ yamale==6.1.0; python_version >= '3.8'
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -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)
@@ -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)