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.
- {python_fx-0.3.2 → python_fx-0.4.0}/CHANGELOG.md +3 -0
- python_fx-0.4.0/CLAUDE.md +121 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/PKG-INFO +22 -22
- {python_fx-0.3.2 → python_fx-0.4.0}/README.md +1 -2
- python_fx-0.4.0/dev-requirements.txt +81 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/pyproject.toml +21 -2
- python_fx-0.4.0/requirements.txt +15 -0
- python_fx-0.4.0/src/pyfx/__version__.py +1 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/app.py +40 -7
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/model.py +7 -1
- python_fx-0.4.0/src/pyfx/model/model_manager.py +210 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/frame.py +1 -1
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/view_manager.py +2 -4
- {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/PKG-INFO +22 -22
- {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/SOURCES.txt +2 -0
- python_fx-0.4.0/src/python_fx.egg-info/requires.txt +17 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/tox.ini +5 -4
- python_fx-0.3.2/dev-requirements.txt +0 -80
- python_fx-0.3.2/requirements.txt +0 -17
- python_fx-0.3.2/src/pyfx/__version__.py +0 -1
- python_fx-0.3.2/src/python_fx.egg-info/requires.txt +0 -27
- {python_fx-0.3.2 → python_fx-0.4.0}/.coveragerc +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/.readthedocs.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/LICENSE.txt +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/MANIFEST.in +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/docs/Makefile +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/docs/conf.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/docs/demo.gif +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/docs/index.rst +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/docs/make.bat +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/setup.cfg +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/setup.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/cli.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/cli_utils.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/config.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/config_parser.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/config.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/config_schema.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/basic.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/emacs.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/keymaps/vim.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/themes/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/config/yaml/themes/basic.yml +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/error.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/autocomplete/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/autocomplete/autocomplete_listener.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPath.g4 +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPath.interp +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPath.tokens +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathLexer.interp +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathLexer.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathLexer.tokens +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathListener.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/JSONPathParser.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/model/common/jsonpath/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/service/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/service/client.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/service/dispatcher.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/popup.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/common/selectable_text.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/abstract_component_keys.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/autocomplete_popup.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/help_popup.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/json_browser.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/query_bar.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/components/warning_bar.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_end_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_end_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_start_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/array/array_unexpanded_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_composite_end_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_composite_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_listbox.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_listwalker.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_node_creator.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_node_factory.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_simple_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/json_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_end_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_end_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_node.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_start_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/object/object_unexpanded_widget.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/__init__.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/base.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/boolean.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/integer.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/null.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/numeric.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/json_lib/primitive/string.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/keys.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/themes.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/view_frame.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/pyfx/view/view_mediator.py +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/dependency_links.txt +0 -0
- {python_fx-0.3.2 → python_fx-0.4.0}/src/python_fx.egg-info/entry_points.txt +0 -0
- {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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fx
|
|
3
|
-
Version: 0.
|
|
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.
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE.txt
|
|
19
|
-
Requires-Dist: antlr4-python3-runtime
|
|
20
|
-
Requires-Dist: asciimatics
|
|
21
|
-
Requires-Dist: click
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist: jsonpath-ng
|
|
25
|
-
Requires-Dist: loguru
|
|
26
|
-
Requires-Dist: overrides
|
|
27
|
-
Requires-Dist: pillow
|
|
28
|
-
Requires-Dist: ply
|
|
29
|
-
Requires-Dist: pyfiglet
|
|
30
|
-
Requires-Dist: pyperclip
|
|
31
|
-
Requires-Dist: pyyaml
|
|
32
|
-
Requires-Dist: typing-extensions
|
|
33
|
-
Requires-Dist: urwid
|
|
34
|
-
Requires-Dist: wcwidth
|
|
35
|
-
Requires-Dist: yamale
|
|
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
|

|
|
39
40
|

|
|
40
41
|

|
|
41
|
-

|
|
42
42
|

|
|
43
43
|

|
|
44
44
|

|
|
@@ -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.
|
|
72
|
+
* python: >= 3.9
|
|
73
73
|
* pip
|
|
74
74
|
|
|
75
75
|
## Installation
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|

|
|
3
3
|

|
|
4
4
|

|
|
5
|
-

|
|
6
5
|

|
|
7
6
|

|
|
8
7
|

|
|
@@ -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.
|
|
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"
|
|
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.
|
|
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
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|