config-cli-gui 0.1.0__tar.gz → 0.1.2__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.
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.idea/runConfigurations/config_generate.xml +2 -2
- config_cli_gui-0.1.2/HISTORY.md +25 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/Makefile +0 -32
- {config_cli_gui-0.1.0/src/config_cli_gui.egg-info → config_cli_gui-0.1.2}/PKG-INFO +1 -1
- config_cli_gui-0.1.2/config.yaml +51 -0
- config_cli_gui-0.1.2/docs/getting-started/install.md +28 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/usage/cli.md +17 -3
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/usage/config.md +8 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui/_version.py +2 -2
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui/cli_generator.py +7 -5
- config_cli_gui-0.1.2/src/config_cli_gui/config_framework.py +281 -0
- config_cli_gui-0.1.2/src/config_cli_gui/docs_generator.py +201 -0
- config_cli_gui-0.1.2/src/config_cli_gui/gui_generator.py +535 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2/src/config_cli_gui.egg-info}/PKG-INFO +1 -1
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui.egg-info/SOURCES.txt +15 -19
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui.egg-info/top_level.txt +0 -1
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/config/config.py +41 -33
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/gui/gui.py +3 -3
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/tests/test_generic_cli.py +10 -14
- config_cli_gui-0.1.0/.github/workflows/build-macos.yml +0 -48
- config_cli_gui-0.1.0/.github/workflows/build.yml +0 -46
- config_cli_gui-0.1.0/.idea/runConfigurations/module_cli.xml +0 -25
- config_cli_gui-0.1.0/.idea/runConfigurations/module_gui.xml +0 -25
- config_cli_gui-0.1.0/HISTORY.md +0 -9
- config_cli_gui-0.1.0/config.yaml +0 -96
- config_cli_gui-0.1.0/docs/develop/pypi_release.md +0 -233
- config_cli_gui-0.1.0/docs/getting-started/install.md +0 -66
- config_cli_gui-0.1.0/src/config_cli_gui/config_framework.py +0 -362
- config_cli_gui-0.1.0/src/config_cli_gui/gui_generator.py +0 -225
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/FUNDING.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/actions/setup-environment/action.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/dependabot.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/init.sh +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/release_message.sh +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/update_funding.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/workflows/main.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/workflows/release.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.github/workflows/update_readme.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.gitignore +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.pre-commit-config.yaml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/.readthedocs.yaml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/LICENSE +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/README.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/.nav.yml +1 -1
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/_static/img/favicon.png +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/_static/img/logo.png +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/css/custom.css +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/develop/contributing.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/develop/make_windows.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/develop/naming_convention.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/funding/funding.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/getting-started/virtual-environment.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/docs/index.md +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/examples/kuhkopfsteig.gpx +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/examples/rother_lilienstein.gpx +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/examples/teneriffa.gpx +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/mkdocs.yml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/pyproject.toml +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/scripts/show_filelist.ps1 +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/scripts/show_tree.ps1 +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/scripts/show_tree.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/scripts/update_readme.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/setup.cfg +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/__init__.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui/__init__.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui.egg-info/dependency_links.txt +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui.egg-info/entry_points.txt +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/config_cli_gui.egg-info/requires.txt +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/src/main.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/template.yml.url +0 -0
- {config_cli_gui-0.1.0/src/example_project → config_cli_gui-0.1.2/tests}/__init__.py +0 -0
- {config_cli_gui-0.1.0/src/example_project/cli → config_cli_gui-0.1.2/tests/example_project}/__init__.py +0 -0
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/__main__.py +0 -0
- {config_cli_gui-0.1.0/src/example_project/config → config_cli_gui-0.1.2/tests/example_project/cli}/__init__.py +0 -0
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/cli/__main__.py +0 -0
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/cli/cli.py +0 -0
- {config_cli_gui-0.1.0/src/example_project/core → config_cli_gui-0.1.2/tests/example_project/config}/__init__.py +0 -0
- {config_cli_gui-0.1.0/src/example_project/gui → config_cli_gui-0.1.2/tests/example_project/core}/__init__.py +0 -0
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/core/base.py +0 -0
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/core/logging.py +0 -0
- {config_cli_gui-0.1.0/tests → config_cli_gui-0.1.2/tests/example_project/gui}/__init__.py +0 -0
- {config_cli_gui-0.1.0/src → config_cli_gui-0.1.2/tests}/example_project/gui/__main__.py +0 -0
- {config_cli_gui-0.1.0 → config_cli_gui-0.1.2}/uv.lock +0 -0
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
<env name="PYTHONUNBUFFERED" value="1" />
|
|
9
9
|
</envs>
|
|
10
10
|
<option name="SDK_HOME" value="" />
|
|
11
|
-
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/
|
|
11
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests/example_project/config" />
|
|
12
12
|
<option name="IS_MODULE_SDK" value="true" />
|
|
13
13
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
14
14
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
15
15
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
|
16
|
-
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/
|
|
16
|
+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/example_project/config/config.py" />
|
|
17
17
|
<option name="PARAMETERS" value="" />
|
|
18
18
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
|
19
19
|
<option name="EMULATE_TERMINAL" value="false" />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Changelog
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
(unreleased)
|
|
6
|
+
------------
|
|
7
|
+
- Feat #3: avoid type_ [Paul Magister]
|
|
8
|
+
- Feat #3: Improved paramter types: [Paul Magister]
|
|
9
|
+
- Feat #3: Improved paramter types: * move example project to tests *
|
|
10
|
+
remove type_ * add improved widgets to gui_generator.py * separate
|
|
11
|
+
module for docs_generator.py. [Paul Magister]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
0.1.0 (2025-06-22)
|
|
15
|
+
------------------
|
|
16
|
+
- Remove unnecessary example files and deps. [Paul Magister]
|
|
17
|
+
- Update README.md from docs/index.md. [github-actions]
|
|
18
|
+
- Fix doc: formatting. [Paul Magister]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
0.0.2 (2025-06-22)
|
|
22
|
+
------------------
|
|
23
|
+
- Remove _version.py. [Paul Magister]
|
|
24
|
+
|
|
25
|
+
|
|
@@ -55,38 +55,6 @@ test: lint ## Run tests and generate coverage report.
|
|
|
55
55
|
uv run coverage xml
|
|
56
56
|
uv run coverage html
|
|
57
57
|
|
|
58
|
-
.PHONY: build-win
|
|
59
|
-
build-win: ## Build the Windows executable.
|
|
60
|
-
echo "Building unified CLI/GUI application"
|
|
61
|
-
uv run pyinstaller --onefile src/main.py --name config-cli-gui --add-data "config.yaml;." --hidden-import config_cli_gui.cli.cli --hidden-import config_cli_gui.gui.gui
|
|
62
|
-
rm -rf release
|
|
63
|
-
mkdir release
|
|
64
|
-
cp dist/config-cli-gui.exe release
|
|
65
|
-
cp config.yaml release
|
|
66
|
-
cp README.md release
|
|
67
|
-
|
|
68
|
-
.PHONY: build-macos
|
|
69
|
-
build-macos: ## Build the macOS executable.
|
|
70
|
-
echo "Building unified CLI/GUI application as executable"
|
|
71
|
-
uv run pyinstaller --onefile src/main.py --name config-cli-gui --add-data "config.yaml:." --hidden-import config_cli_gui.cli.cli --hidden-import config_cli_gui.gui.gui
|
|
72
|
-
|
|
73
|
-
echo "Building unified CLI/GUI application as .app bundle"
|
|
74
|
-
# --windowed is important to hide the console for GUI mode
|
|
75
|
-
# The name "TemplateApp" becomes the name of the .app
|
|
76
|
-
uv run pyinstaller --windowed --name "TemplateApp" src/main.py --add-data "config.yaml:." --hidden-import config_cli_gui.cli.cli --hidden-import config_cli_gui.gui.gui
|
|
77
|
-
|
|
78
|
-
# Prepare ZIP file for release
|
|
79
|
-
rm -rf release
|
|
80
|
-
mkdir release
|
|
81
|
-
echo "Copy the CLI/GUI executable"
|
|
82
|
-
cp dist/config-cli-gui release/
|
|
83
|
-
echo "Copy the .app bundle (directory) recursively"
|
|
84
|
-
cp -R "dist/TemplateApp.app" release/
|
|
85
|
-
echo "Copy configuration and documentation"
|
|
86
|
-
cp config.yaml release/
|
|
87
|
-
cp README.md release/
|
|
88
|
-
echo "Create usage instructions"
|
|
89
|
-
|
|
90
58
|
.PHONY: watch
|
|
91
59
|
watch: ## Run tests on every change.
|
|
92
60
|
ls **/**.py | entr uv run pytest -s -vvv -l --tb=long --maxfail=1 tests/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
app:
|
|
2
|
+
# Date format to use | type=ConfigParameter, default=%Y-%m-%d
|
|
3
|
+
date_format: '%Y-%m-%d'
|
|
4
|
+
# Enable logging to console | type=ConfigParameter, default=True
|
|
5
|
+
enable_console_logging: true
|
|
6
|
+
# Enable logging to file | type=ConfigParameter, default=True
|
|
7
|
+
enable_file_logging: true
|
|
8
|
+
# Number of backup log files to keep | type=ConfigParameter, default=5
|
|
9
|
+
log_backup_count: 5
|
|
10
|
+
# Maximum log file size in MB before rotation | type=ConfigParameter, default=10
|
|
11
|
+
log_file_max_size: 10
|
|
12
|
+
# Log message format style | type=ConfigParameter, default=detailed
|
|
13
|
+
log_format: detailed
|
|
14
|
+
# Logging level for the application | type=ConfigParameter, default=INFO
|
|
15
|
+
log_level: INFO
|
|
16
|
+
# Maximum number of worker threads | type=ConfigParameter, default=4
|
|
17
|
+
max_workers: 4
|
|
18
|
+
cli:
|
|
19
|
+
# Include elevation data in waypoints | type=ConfigParameter, default=True
|
|
20
|
+
elevation: true
|
|
21
|
+
# Extract starting points of each track as waypoint | type=ConfigParameter, default=True
|
|
22
|
+
extract_waypoints: true
|
|
23
|
+
# Path to input (file or folder) | type=ConfigParameter, default=
|
|
24
|
+
input: ''
|
|
25
|
+
# Maximum distance between two waypoints | type=ConfigParameter, default=20
|
|
26
|
+
min_dist: 20
|
|
27
|
+
# Path to output destination | type=ConfigParameter, default=
|
|
28
|
+
output: ''
|
|
29
|
+
gui:
|
|
30
|
+
# Automatically scroll to newest log entries | type=ConfigParameter, default=True
|
|
31
|
+
auto_scroll_log: true
|
|
32
|
+
# Height of the log window in pixels | type=ConfigParameter, default=200
|
|
33
|
+
log_window_height: 200
|
|
34
|
+
# Maximum number of log lines to keep in GUI | type=ConfigParameter, default=1000
|
|
35
|
+
max_log_lines: 1000
|
|
36
|
+
# GUI theme setting | type=ConfigParameter, default=light
|
|
37
|
+
theme: light
|
|
38
|
+
# Default window height | type=ConfigParameter, default=600
|
|
39
|
+
window_height: 600
|
|
40
|
+
# Default window width | type=ConfigParameter, default=800
|
|
41
|
+
window_width: 800
|
|
42
|
+
misc:
|
|
43
|
+
# Color setting for the application | type=ConfigParameter, default=#ff0000
|
|
44
|
+
some_color:
|
|
45
|
+
- 255
|
|
46
|
+
- 0
|
|
47
|
+
- 0
|
|
48
|
+
# Date setting for the application | type=ConfigParameter, default=2025-06-23 18:58:52.946103
|
|
49
|
+
some_date: '2025-06-23T18:58:52.946103'
|
|
50
|
+
# Path to the file to use | type=ConfigParameter, default=some_file.txt
|
|
51
|
+
some_file: some_file.txt
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## 🐍 PyPI
|
|
5
|
+
|
|
6
|
+
### Install the package from PyPI
|
|
7
|
+
|
|
8
|
+
Download from [PyPI](https://pypi.org/):
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install config-cli-gui
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 👩🏼💻 Run from source
|
|
15
|
+
|
|
16
|
+
### Clone the repository
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Install dependencies
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv venv
|
|
26
|
+
uv pip install -e .[dev,docs]
|
|
27
|
+
```
|
|
28
|
+
|
|
@@ -26,19 +26,33 @@ python -m app [OPTIONS] input
|
|
|
26
26
|
python -m app input
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
### 2. With
|
|
29
|
+
### 2. With verbose logging
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m app -v input
|
|
33
|
+
python -m app --verbose input
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. With quiet mode
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m app -q input
|
|
40
|
+
python -m app --quiet input
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 4. With min_dist parameter
|
|
30
44
|
|
|
31
45
|
```bash
|
|
32
46
|
python -m app --min_dist 20 input
|
|
33
47
|
```
|
|
34
48
|
|
|
35
|
-
###
|
|
49
|
+
### 5. With extract_waypoints parameter
|
|
36
50
|
|
|
37
51
|
```bash
|
|
38
52
|
python -m app --extract_waypoints True input
|
|
39
53
|
```
|
|
40
54
|
|
|
41
|
-
###
|
|
55
|
+
### 6. With elevation parameter
|
|
42
56
|
|
|
43
57
|
```bash
|
|
44
58
|
python -m app --elevation True input
|
|
@@ -37,3 +37,11 @@ The parameters in the cli category can be accessed via the command line interfac
|
|
|
37
37
|
| auto_scroll_log | bool | Automatically scroll to newest log entries | True | [True, False] |
|
|
38
38
|
| max_log_lines | int | Maximum number of log lines to keep in GUI | 1000 | - |
|
|
39
39
|
|
|
40
|
+
## Category "misc"
|
|
41
|
+
|
|
42
|
+
| Name | Type | Description | Default | Choices |
|
|
43
|
+
|------------|-------------|-----------------------------------|----------------------------------------------------|---------|
|
|
44
|
+
| some_file | WindowsPath | Path to the file to use | WindowsPath('some_file.txt') | - |
|
|
45
|
+
| some_color | Color | Color setting for the application | Color(255, 0, 0) | - |
|
|
46
|
+
| some_date | datetime | Date setting for the application | datetime.datetime(2025, 6, 23, 18, 58, 52, 946103) | - |
|
|
47
|
+
|
|
@@ -46,6 +46,8 @@ class CliGenerator:
|
|
|
46
46
|
|
|
47
47
|
# Generate arguments from CLI config parameters
|
|
48
48
|
for param in cli_params:
|
|
49
|
+
param_type = type(param.default)
|
|
50
|
+
|
|
49
51
|
if param.required and param.cli_arg is None:
|
|
50
52
|
# Positional argument
|
|
51
53
|
parser.add_argument(param.name, help=param.help)
|
|
@@ -57,17 +59,17 @@ class CliGenerator:
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
# Handle different parameter types
|
|
60
|
-
if param.choices and
|
|
62
|
+
if param.choices and param_type != bool:
|
|
61
63
|
kwargs["choices"] = param.choices
|
|
62
64
|
|
|
63
|
-
if
|
|
65
|
+
if param_type == int:
|
|
64
66
|
kwargs["type"] = int
|
|
65
|
-
elif
|
|
67
|
+
elif param_type == float:
|
|
66
68
|
kwargs["type"] = float
|
|
67
|
-
elif
|
|
69
|
+
elif param_type == bool:
|
|
68
70
|
kwargs["action"] = "store_true" if not param.default else "store_false"
|
|
69
71
|
kwargs["help"] = f"{param.help} (default: {param.default})"
|
|
70
|
-
elif
|
|
72
|
+
elif param_type == str:
|
|
71
73
|
kwargs["type"] = str
|
|
72
74
|
|
|
73
75
|
parser.add_argument(param.cli_arg, **kwargs)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Color:
|
|
13
|
+
"""Simple color class for RGB values."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, r: int = 0, g: int = 0, b: int = 0):
|
|
16
|
+
self.r = max(0, min(255, r))
|
|
17
|
+
self.g = max(0, min(255, g))
|
|
18
|
+
self.b = max(0, min(255, b))
|
|
19
|
+
|
|
20
|
+
def to_list(self) -> list[int]:
|
|
21
|
+
return [self.r, self.g, self.b]
|
|
22
|
+
|
|
23
|
+
def to_hex(self) -> str:
|
|
24
|
+
return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_list(cls, rgb_list: list[int]) -> "Color":
|
|
28
|
+
if len(rgb_list) >= 3:
|
|
29
|
+
return cls(rgb_list[0], rgb_list[1], rgb_list[2])
|
|
30
|
+
return cls()
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_hex(cls, hex_color: str) -> "Color":
|
|
34
|
+
hex_color = hex_color.lstrip("#")
|
|
35
|
+
if len(hex_color) == 6:
|
|
36
|
+
return cls(int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16))
|
|
37
|
+
return cls()
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
return self.to_hex()
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return f"Color({self.r}, {self.g}, {self.b})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ConfigParameter:
|
|
48
|
+
"""Represents a single configuration parameter with all its metadata."""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
default: Any
|
|
52
|
+
choices: list | tuple | None = None
|
|
53
|
+
help: str = ""
|
|
54
|
+
cli_arg: str = None
|
|
55
|
+
required: bool = False
|
|
56
|
+
is_cli: bool = False
|
|
57
|
+
category: str = "general"
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
if self.is_cli and self.cli_arg is None and not self.required:
|
|
61
|
+
self.cli_arg = f"--{self.name}"
|
|
62
|
+
if isinstance(self.default, bool) and self.choices is None:
|
|
63
|
+
self.choices = [True, False]
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def type_(self) -> type:
|
|
67
|
+
"""Get the type from the default value."""
|
|
68
|
+
return type(self.default)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BaseConfigCategory(BaseModel, ABC):
|
|
72
|
+
"""Base class for configuration categories."""
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def get_category_name(self) -> str:
|
|
76
|
+
"""Return the category name for this configuration group."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def get_parameters(self) -> list[ConfigParameter]:
|
|
80
|
+
"""Get all ConfigParameter objects from this category."""
|
|
81
|
+
parameters = []
|
|
82
|
+
for field_name in self.__class__.model_fields:
|
|
83
|
+
param = getattr(self, field_name)
|
|
84
|
+
if isinstance(param, ConfigParameter):
|
|
85
|
+
param.category = self.get_category_name()
|
|
86
|
+
parameters.append(param)
|
|
87
|
+
return parameters
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ConfigManager:
|
|
91
|
+
"""Generic configuration manager that can handle multiple configuration categories."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self, categories: tuple[BaseConfigCategory, ...], config_file: str = None, **kwargs
|
|
95
|
+
):
|
|
96
|
+
"""Initialize configuration manager.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
config_file: Path to configuration file (JSON or YAML)
|
|
100
|
+
**kwargs: Override parameters in format category__parameter
|
|
101
|
+
"""
|
|
102
|
+
self._categories: dict[str, BaseConfigCategory] = {}
|
|
103
|
+
|
|
104
|
+
for category in categories:
|
|
105
|
+
if isinstance(category, BaseConfigCategory):
|
|
106
|
+
self.add_category(category.get_category_name(), category)
|
|
107
|
+
else:
|
|
108
|
+
raise TypeError(
|
|
109
|
+
f"Category must be an instance of BaseConfigCategory, got {type(category)}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Load from file if provided
|
|
113
|
+
if config_file:
|
|
114
|
+
self.load_from_file(config_file)
|
|
115
|
+
|
|
116
|
+
# Override with provided kwargs
|
|
117
|
+
self._apply_kwargs(kwargs)
|
|
118
|
+
|
|
119
|
+
def add_category(self, name: str, category: BaseConfigCategory):
|
|
120
|
+
"""Add a configuration category.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: Name of the category (e.g., 'app', 'database', 'gui')
|
|
124
|
+
category: Configuration category instance
|
|
125
|
+
"""
|
|
126
|
+
self._categories[name] = category
|
|
127
|
+
|
|
128
|
+
def get_category(self, name: str) -> BaseConfigCategory:
|
|
129
|
+
"""Get a configuration category by name."""
|
|
130
|
+
return self._categories.get(name)
|
|
131
|
+
|
|
132
|
+
def _apply_kwargs(self, kwargs: dict[str, Any]):
|
|
133
|
+
"""Apply keyword arguments to override configuration values."""
|
|
134
|
+
for key, value in kwargs.items():
|
|
135
|
+
if "__" in key:
|
|
136
|
+
category_name, param_name = key.split("__", 1)
|
|
137
|
+
if category_name in self._categories:
|
|
138
|
+
category = self._categories[category_name]
|
|
139
|
+
if hasattr(category, param_name):
|
|
140
|
+
param = getattr(category, param_name)
|
|
141
|
+
if isinstance(param, ConfigParameter):
|
|
142
|
+
param.default = value
|
|
143
|
+
|
|
144
|
+
def load_from_file(self, config_file: str):
|
|
145
|
+
"""Load configuration from JSON or YAML file."""
|
|
146
|
+
config_path = Path(config_file)
|
|
147
|
+
if not config_path.exists():
|
|
148
|
+
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
149
|
+
|
|
150
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
151
|
+
if config_path.suffix.lower() in [".yml", ".yaml"]:
|
|
152
|
+
config_data = yaml.safe_load(f)
|
|
153
|
+
else:
|
|
154
|
+
config_data = json.load(f)
|
|
155
|
+
|
|
156
|
+
# Store loaded data for later application
|
|
157
|
+
self._apply_config_data(config_data)
|
|
158
|
+
|
|
159
|
+
def _apply_config_data(self, _loaded_config_data):
|
|
160
|
+
"""Apply configuration data to categories."""
|
|
161
|
+
|
|
162
|
+
# Apply loaded configuration
|
|
163
|
+
for category_name, category_data in _loaded_config_data.items():
|
|
164
|
+
if category_name in self._categories:
|
|
165
|
+
category = self._categories[category_name]
|
|
166
|
+
for param_name, param_value in category_data.items():
|
|
167
|
+
if hasattr(category, param_name):
|
|
168
|
+
param = getattr(category, param_name)
|
|
169
|
+
if isinstance(param, ConfigParameter):
|
|
170
|
+
# Handle special types
|
|
171
|
+
if isinstance(param.default, Color) and isinstance(param_value, list):
|
|
172
|
+
param.default = Color.from_list(param_value)
|
|
173
|
+
elif isinstance(param.default, Path):
|
|
174
|
+
param.default = Path(param_value)
|
|
175
|
+
elif isinstance(param.default, datetime):
|
|
176
|
+
param.default = datetime.fromisoformat(param_value)
|
|
177
|
+
else:
|
|
178
|
+
param.default = param_value
|
|
179
|
+
|
|
180
|
+
def save_to_file(self, config_file: str, format_: str = "auto"):
|
|
181
|
+
"""Save current configuration to file with enhanced YAML formatting and comments.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
config_file (str): The path to the configuration file.
|
|
185
|
+
format_ (str): The format to save the file in ('auto', 'json', 'yaml').
|
|
186
|
+
"""
|
|
187
|
+
config_path = Path(config_file)
|
|
188
|
+
config_data = self.to_dict()
|
|
189
|
+
|
|
190
|
+
# Determine format
|
|
191
|
+
if format_ == "auto":
|
|
192
|
+
format_ = "yaml" if config_path.suffix.lower() in [".yml", ".yaml"] else "json"
|
|
193
|
+
|
|
194
|
+
# Ensure directory exists
|
|
195
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
|
|
197
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
198
|
+
if format_ == "yaml":
|
|
199
|
+
yaml.dump(config_data, f, default_flow_style=False, indent=2)
|
|
200
|
+
else:
|
|
201
|
+
json.dump(config_data, f, indent=2)
|
|
202
|
+
|
|
203
|
+
# Append comments for YAML files
|
|
204
|
+
if format_ == "yaml":
|
|
205
|
+
self._append_comments_to_yaml(config_path)
|
|
206
|
+
|
|
207
|
+
def to_dict(self) -> dict[str, Any]:
|
|
208
|
+
"""Convert configuration to dictionary."""
|
|
209
|
+
result = {}
|
|
210
|
+
for category_name, category in self._categories.items():
|
|
211
|
+
category_dict = {}
|
|
212
|
+
for param in category.get_parameters():
|
|
213
|
+
value = param.default
|
|
214
|
+
# Handle special types for serialization
|
|
215
|
+
if isinstance(value, Color):
|
|
216
|
+
value = value.to_list()
|
|
217
|
+
elif isinstance(value, Path):
|
|
218
|
+
value = str(value)
|
|
219
|
+
elif isinstance(value, datetime):
|
|
220
|
+
value = value.isoformat()
|
|
221
|
+
category_dict[param.name] = value
|
|
222
|
+
result[category_name] = category_dict
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
def get_all_parameters(self) -> list[ConfigParameter]:
|
|
226
|
+
"""Get all parameters from all categories."""
|
|
227
|
+
parameters = []
|
|
228
|
+
for category in self._categories.values():
|
|
229
|
+
parameters.extend(category.get_parameters())
|
|
230
|
+
return parameters
|
|
231
|
+
|
|
232
|
+
def get_cli_parameters(self) -> list[ConfigParameter]:
|
|
233
|
+
"""Get parameters that are CLI-enabled."""
|
|
234
|
+
cli_parameters = []
|
|
235
|
+
for category in self._categories.values():
|
|
236
|
+
for param in category.get_parameters():
|
|
237
|
+
if param.is_cli:
|
|
238
|
+
cli_parameters.append(param)
|
|
239
|
+
return cli_parameters
|
|
240
|
+
|
|
241
|
+
def _append_comments_to_yaml(self, config_path: Path):
|
|
242
|
+
"""Appends comments to a YAML file based on ConfigParameter metadata.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
config_path (Path): The path to the YAML configuration file.
|
|
246
|
+
"""
|
|
247
|
+
lines = config_path.read_text(encoding="utf-8").splitlines()
|
|
248
|
+
new_lines = []
|
|
249
|
+
all_parameters = {param.name: param for param in self.get_all_parameters()}
|
|
250
|
+
current_category = None
|
|
251
|
+
|
|
252
|
+
for line in lines:
|
|
253
|
+
stripped_line = line.strip()
|
|
254
|
+
# Check for category (e.g., 'app:')
|
|
255
|
+
# A category should end with ':', not start with '#', and not be indented.
|
|
256
|
+
if (
|
|
257
|
+
stripped_line.endswith(":")
|
|
258
|
+
and not stripped_line.startswith("#")
|
|
259
|
+
and line.startswith(stripped_line)
|
|
260
|
+
):
|
|
261
|
+
current_category = stripped_line[:-1]
|
|
262
|
+
new_lines.append(line)
|
|
263
|
+
else:
|
|
264
|
+
# Check for parameter (e.g., ' date_format: '%Y-%m-%d'')
|
|
265
|
+
# This needs to handle cases where the value spans multiple lines
|
|
266
|
+
parts = stripped_line.split(":", 1)
|
|
267
|
+
if len(parts) > 1: # This line might be a parameter definition
|
|
268
|
+
param_name = parts[0].strip()
|
|
269
|
+
if param_name in all_parameters:
|
|
270
|
+
param = all_parameters[param_name]
|
|
271
|
+
# Ensure the parameter belongs to the current category
|
|
272
|
+
# and is not a sub-item of a multi-line value
|
|
273
|
+
if current_category and param.category == current_category:
|
|
274
|
+
comment_indent = " " * (len(line) - len(stripped_line))
|
|
275
|
+
comment = (
|
|
276
|
+
f"{comment_indent}# {param.help} | "
|
|
277
|
+
f"type={type(param).__name__}, default={param.default}"
|
|
278
|
+
)
|
|
279
|
+
new_lines.append(comment)
|
|
280
|
+
new_lines.append(line)
|
|
281
|
+
config_path.write_text("\n".join(new_lines), encoding="utf-8")
|