config-cli-gui 0.2.9__tar.gz → 0.3.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.
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/HISTORY.md +20 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/PKG-INFO +1 -1
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/README.md +42 -38
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/config.yaml +36 -39
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/_version.py +3 -3
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/cli.py +13 -8
- config_cli_gui-0.3.0/src/config_cli_gui/config.py +251 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/configtypes/font.py +35 -1
- config_cli_gui-0.3.0/src/config_cli_gui/logging.py +216 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui.egg-info/PKG-INFO +1 -1
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui.egg-info/SOURCES.txt +1 -1
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/cli/cli_example.py +29 -8
- config_cli_gui-0.3.0/tests/example_project/core/base.py +116 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/gui/gui_example.py +4 -4
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/test_cli.py +1 -1
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/test_config_manager.py +6 -1
- config_cli_gui-0.2.9/src/config_cli_gui/config.py +0 -223
- config_cli_gui-0.2.9/tests/example_project/core/base.py +0 -55
- config_cli_gui-0.2.9/tests/example_project/core/logging.py +0 -219
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/FUNDING.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/actions/setup-environment/action.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/dependabot.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/init.sh +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/release_message.sh +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/update_funding.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/workflows/main.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/workflows/release.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.github/workflows/update_readme.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.gitignore +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.idea/runConfigurations/config_generate.xml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.idea/runConfigurations/example_project_cli.xml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.idea/runConfigurations/example_project_gui.xml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.pre-commit-config.yaml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/.readthedocs.yaml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/LICENSE +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/Makefile +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/.nav.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/_static/img/favicon.png +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/_static/img/logo.png +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/css/custom.css +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/develop/contributing.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/develop/make_windows.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/develop/naming_convention.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/funding/funding.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/getting-started/install.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/getting-started/virtual-environment.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/index.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/usage/cli.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/docs/usage/config.md +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/mkdocs.yml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/pyproject.toml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/scripts/show_filelist.ps1 +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/scripts/show_tree.ps1 +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/scripts/show_tree.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/scripts/update_readme.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/setup.cfg +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/configtypes/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/configtypes/color.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/configtypes/vector.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/docs.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui/gui.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui.egg-info/dependency_links.txt +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui.egg-info/entry_points.txt +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui.egg-info/requires.txt +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/src/config_cli_gui.egg-info/top_level.txt +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/template.yml.url +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/__main__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/cli/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/cli/__main__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/config/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/config/config_example.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/core/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/example.gpx +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/gui/__init__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/gui/__main__.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/example_project/gui/config.yaml +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/test_docs.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/tests/test_generic_cli.py +0 -0
- {config_cli_gui-0.2.9 → config_cli_gui-0.3.0}/uv.lock +0 -0
|
@@ -4,6 +4,26 @@ Changelog
|
|
|
4
4
|
|
|
5
5
|
(unreleased)
|
|
6
6
|
------------
|
|
7
|
+
- Docs: Update HISTORY.md for release 0.2.9. [Paul Magister]
|
|
8
|
+
- Refactoring: config parameters will stay ordered like in the config.
|
|
9
|
+
[Paul Magister]
|
|
10
|
+
- Refactoring: config parameters will stay ordered like in the config.
|
|
11
|
+
[Paul Magister]
|
|
12
|
+
- Refactor cli.py, move logging into this project. [Paul Magister]
|
|
13
|
+
- Docs: Update HISTORY.md for release 0.2.9. [Paul Magister]
|
|
14
|
+
- Docs: Update HISTORY.md for release 0.2.10. [Paul Magister]
|
|
15
|
+
- Docs: Update HISTORY.md for release 0.2.9. [Paul Magister]
|
|
16
|
+
- Update README.md from docs/index.md. [github-actions]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
0.2.10 (2025-12-03)
|
|
20
|
+
-------------------
|
|
21
|
+
- Docs: Update HISTORY.md for release 0.2.10. [Paul Magister]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
0.2.9 (2025-12-03)
|
|
25
|
+
------------------
|
|
26
|
+
- Docs: Update HISTORY.md for release 0.2.9. [Paul Magister]
|
|
7
27
|
- Spinbox for vector. [Paul Magister]
|
|
8
28
|
- Better example, make get_image_font sensitive to dpi, [Paul Magister]
|
|
9
29
|
|
|
@@ -30,11 +30,6 @@ You can install `config-cli-gui` using pip:
|
|
|
30
30
|
pip install config-cli-gui
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
## Contribution
|
|
34
|
-
|
|
35
|
-
Refer to this how-to in the referenced project for getting started to install and develop on this project:
|
|
36
|
-
https://github.com/pamagister/python-template-project
|
|
37
|
-
|
|
38
33
|
---
|
|
39
34
|
|
|
40
35
|
## ✨ Features
|
|
@@ -67,44 +62,27 @@ from config_cli_gui.config import (
|
|
|
67
62
|
ConfigParameter,
|
|
68
63
|
)
|
|
69
64
|
from config_cli_gui.configtypes.color import Color
|
|
70
|
-
from config_cli_gui.
|
|
71
|
-
|
|
65
|
+
from config_cli_gui.configtypes.font import Font
|
|
66
|
+
from config_cli_gui.configtypes.vector import Vector
|
|
72
67
|
|
|
73
|
-
class CliConfig(ConfigCategory):
|
|
74
|
-
"""CLI-specific configuration parameters."""
|
|
75
68
|
|
|
69
|
+
class MiscConfig(ConfigCategory):
|
|
76
70
|
def get_category_name(self) -> str:
|
|
77
|
-
return "
|
|
78
|
-
|
|
79
|
-
# Positional argument
|
|
80
|
-
input: ConfigParameter = ConfigParameter(
|
|
81
|
-
name="input",
|
|
82
|
-
value="",
|
|
83
|
-
help="Path to input (file or folder)",
|
|
84
|
-
required=True,
|
|
85
|
-
is_cli=True,
|
|
86
|
-
)
|
|
71
|
+
return "misc"
|
|
87
72
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
help="Path to output destination",
|
|
73
|
+
some_numeric: ConfigParameter = ConfigParameter(
|
|
74
|
+
name="some_numeric",
|
|
75
|
+
value=int(42),
|
|
76
|
+
help="Example integer",
|
|
93
77
|
is_cli=True,
|
|
94
78
|
)
|
|
95
79
|
|
|
96
|
-
|
|
97
|
-
name="
|
|
98
|
-
value=
|
|
99
|
-
help="
|
|
100
|
-
is_cli=True,
|
|
80
|
+
some_vector: ConfigParameter = ConfigParameter(
|
|
81
|
+
name="some_vector",
|
|
82
|
+
value=Vector(1, 2, 3),
|
|
83
|
+
help="Example vector",
|
|
101
84
|
)
|
|
102
85
|
|
|
103
|
-
|
|
104
|
-
class MiscConfig(ConfigCategory):
|
|
105
|
-
def get_category_name(self) -> str:
|
|
106
|
-
return "misc"
|
|
107
|
-
|
|
108
86
|
some_file: ConfigParameter = ConfigParameter(
|
|
109
87
|
name="some_file",
|
|
110
88
|
value=Path("some_file.txt"),
|
|
@@ -119,20 +97,46 @@ class MiscConfig(ConfigCategory):
|
|
|
119
97
|
|
|
120
98
|
some_date: ConfigParameter = ConfigParameter(
|
|
121
99
|
name="some_date",
|
|
122
|
-
value=datetime.
|
|
100
|
+
value=datetime.fromisoformat("2025-12-31 10:30:45"),
|
|
123
101
|
help="Date setting for the application",
|
|
124
102
|
)
|
|
125
103
|
|
|
104
|
+
some_font: ConfigParameter = ConfigParameter(
|
|
105
|
+
name="some_font",
|
|
106
|
+
value=Font("DejaVuSans.ttf", size=12, color=Color(0, 0, 255)),
|
|
107
|
+
help="Font setting for the application",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AppConfig(ConfigCategory):
|
|
112
|
+
"""Application-specific configuration parameters."""
|
|
113
|
+
|
|
114
|
+
def get_category_name(self) -> str:
|
|
115
|
+
return "app"
|
|
116
|
+
|
|
117
|
+
log_level: ConfigParameter = ConfigParameter(
|
|
118
|
+
name="log_level",
|
|
119
|
+
value="INFO",
|
|
120
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
121
|
+
help="Logging level for the application",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
log_file_max_size: ConfigParameter = ConfigParameter(
|
|
125
|
+
name="log_file_max_size",
|
|
126
|
+
value=10,
|
|
127
|
+
help="Maximum log file size in MB before rotation",
|
|
128
|
+
)
|
|
129
|
+
|
|
126
130
|
|
|
127
131
|
class ProjectConfigManager(ConfigManager): # Inherit from ConfigManager
|
|
128
132
|
"""Main configuration manager that handles all parameter categories."""
|
|
129
133
|
|
|
130
|
-
|
|
134
|
+
app: AppConfig
|
|
131
135
|
misc: MiscConfig
|
|
132
|
-
|
|
136
|
+
|
|
133
137
|
def __init__(self, config_file: str | None = None, **kwargs):
|
|
134
138
|
"""Initialize the configuration manager with all parameter categories."""
|
|
135
|
-
categories = (
|
|
139
|
+
categories = (MiscConfig(), AppConfig())
|
|
136
140
|
super().__init__(categories, config_file, **kwargs)
|
|
137
141
|
|
|
138
142
|
|
|
@@ -1,63 +1,60 @@
|
|
|
1
|
+
cli:
|
|
2
|
+
# Path to input (file or folder) | type=str | [CLI]
|
|
3
|
+
input: ''
|
|
4
|
+
# Path to output destination | type=str | [CLI]
|
|
5
|
+
output: ''
|
|
6
|
+
# Maximum distance between two waypoints | type=int | [CLI]
|
|
7
|
+
min_dist: 20
|
|
8
|
+
# Extract starting points of each track as waypoint | type=bool | [CLI] | choices=[True, False]
|
|
9
|
+
extract_waypoints: true
|
|
10
|
+
# Include elevation data in waypoints | type=bool | [CLI] | choices=[True, False]
|
|
11
|
+
elevation: false
|
|
1
12
|
app:
|
|
2
13
|
# Date format to use | type=str
|
|
3
14
|
date_format: '%Y-%m-%d'
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
# Enable logging to file | type=bool | choices=[True, False]
|
|
7
|
-
enable_file_logging: true
|
|
8
|
-
# Number of backup log files to keep | type=int
|
|
9
|
-
log_backup_count: 5
|
|
15
|
+
# Logging level for the application | type=str | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
16
|
+
log_level: INFO
|
|
10
17
|
# Maximum log file size in MB before rotation | type=int
|
|
11
18
|
log_file_max_size: 10
|
|
19
|
+
# Number of backup log files to keep | type=int
|
|
20
|
+
log_backup_count: 5
|
|
12
21
|
# Log message format style | type=str | choices=['simple', 'detailed', 'json']
|
|
13
22
|
log_format: detailed
|
|
14
|
-
# Logging level for the application | type=str | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
|
15
|
-
log_level: INFO
|
|
16
23
|
# Maximum number of worker threads | type=int
|
|
17
24
|
max_workers: 4
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
extract_waypoints: true
|
|
23
|
-
# Path to input (file or folder) | type=str [CLI]
|
|
24
|
-
input: ''
|
|
25
|
-
# Maximum distance between two waypoints | type=int [CLI]
|
|
26
|
-
min_dist: 20
|
|
27
|
-
# Path to output destination | type=str [CLI]
|
|
28
|
-
output: ''
|
|
25
|
+
# Enable logging to file | type=bool | choices=[True, False]
|
|
26
|
+
enable_file_logging: true
|
|
27
|
+
# Enable logging to console | type=bool | choices=[True, False]
|
|
28
|
+
enable_console_logging: true
|
|
29
29
|
gui:
|
|
30
|
-
#
|
|
31
|
-
|
|
30
|
+
# GUI theme setting | type=str | choices=['light', 'dark', 'auto']
|
|
31
|
+
theme: light
|
|
32
|
+
# Default window width | type=int
|
|
33
|
+
window_width: 800
|
|
34
|
+
# Default window height | type=int
|
|
35
|
+
window_height: 600
|
|
32
36
|
# Height of the log window in pixels | type=int
|
|
33
37
|
log_window_height: 200
|
|
38
|
+
# Automatically scroll to the newest log entries | type=bool | choices=[True, False]
|
|
39
|
+
auto_scroll_log: true
|
|
34
40
|
# Maximum number of log lines to keep in GUI | type=int
|
|
35
41
|
max_log_lines: 1000
|
|
36
42
|
# Point in 2D space | type=Vector
|
|
37
43
|
point2D: (7, 11)
|
|
38
44
|
# Point in 3D space | type=Vector
|
|
39
45
|
point3D: (1.2, 3.4, 5.6)
|
|
40
|
-
# GUI theme setting | type=str | choices=['light', 'dark', 'auto']
|
|
41
|
-
theme: light
|
|
42
|
-
# Default window height | type=int
|
|
43
|
-
window_height: 600
|
|
44
|
-
# Default window width | type=int
|
|
45
|
-
window_width: 800
|
|
46
46
|
misc:
|
|
47
|
-
# Color setting for the application | type=Color
|
|
48
|
-
some_color: '#ff0000'
|
|
49
|
-
# Date setting for the application | type=datetime
|
|
50
|
-
some_date: '2025-12-31T10:30:45'
|
|
51
|
-
# Path to the file to use | type=PosixPath
|
|
52
|
-
some_file: some_file.txt
|
|
53
|
-
# Font setting for the application | type=Font
|
|
54
|
-
some_font:
|
|
55
|
-
- DejaVuSans.ttf
|
|
56
|
-
- 12
|
|
57
|
-
- '#0000ff'
|
|
58
47
|
# Example integer | type=int
|
|
59
48
|
some_numeric: 42
|
|
60
49
|
# Example vector 2D | type=Vector
|
|
61
50
|
some_vector2d: (1, 2)
|
|
62
51
|
# Example vector 3D | type=Vector
|
|
63
|
-
some_vector3d: (1.1, 2.2, 3.3)
|
|
52
|
+
some_vector3d: (1.1, 2.2, 3.3)
|
|
53
|
+
# Path to the file to use | type=PosixPath
|
|
54
|
+
some_file: some_file.txt
|
|
55
|
+
# Color setting for the application | type=Color
|
|
56
|
+
some_color: '#ff0000'
|
|
57
|
+
# Date setting for the application | type=datetime
|
|
58
|
+
some_date: '2025-12-31T10:30:45'
|
|
59
|
+
# Font setting for the application | type=Font
|
|
60
|
+
some_font: 'DejaVuSans.ttf, 12, #0000ff'
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.3.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g10ea5e914'
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import argparse
|
|
5
5
|
import traceback
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from logging import Logger, getLogger
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from config_cli_gui.config import ConfigManager
|
|
@@ -103,13 +104,17 @@ class CliGenerator:
|
|
|
103
104
|
# ----------------------------------------------------------------------
|
|
104
105
|
def run_cli(
|
|
105
106
|
self,
|
|
106
|
-
main_function: Callable[[ConfigManager], int],
|
|
107
|
+
main_function: Callable[[ConfigManager, Logger], int],
|
|
107
108
|
description: str = None,
|
|
108
|
-
validator: Callable[[ConfigManager], bool] = None,
|
|
109
|
+
validator: Callable[[ConfigManager, Logger], bool] = None,
|
|
110
|
+
logger: Logger = None,
|
|
109
111
|
) -> int:
|
|
110
112
|
parser = self.create_argument_parser(description)
|
|
111
113
|
args = parser.parse_args()
|
|
112
114
|
|
|
115
|
+
if logger is None:
|
|
116
|
+
logger = getLogger(self.app_name)
|
|
117
|
+
|
|
113
118
|
# Load config_file only ONCE
|
|
114
119
|
config = ConfigManager(
|
|
115
120
|
categories=tuple(self.config_manager._categories.values()),
|
|
@@ -121,17 +126,17 @@ class CliGenerator:
|
|
|
121
126
|
config.apply_overrides(overrides)
|
|
122
127
|
|
|
123
128
|
# Optional validation
|
|
124
|
-
if validator and not validator(config):
|
|
125
|
-
|
|
129
|
+
if validator and not validator(config, logger):
|
|
130
|
+
logger.error("Configuration validation failed.")
|
|
126
131
|
return 1
|
|
127
132
|
|
|
128
133
|
# Execute main
|
|
129
134
|
try:
|
|
130
|
-
return main_function(config)
|
|
135
|
+
return main_function(config, logger)
|
|
131
136
|
except KeyboardInterrupt:
|
|
132
|
-
|
|
137
|
+
logger.info("Interrupted.")
|
|
133
138
|
return 130
|
|
134
139
|
except Exception as e:
|
|
135
|
-
|
|
136
|
-
traceback.
|
|
140
|
+
logger.error(f"Unexpected error: {e}")
|
|
141
|
+
logger.debug(traceback.format_exc())
|
|
137
142
|
return 1
|
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
from config_cli_gui.configtypes.color import Color
|
|
12
|
+
from config_cli_gui.configtypes.font import Font
|
|
13
|
+
from config_cli_gui.configtypes.vector import Vector
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ConfigParameter:
|
|
18
|
+
"""Represents a single configuration parameter with metadata."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
value: Any
|
|
22
|
+
choices: list[Any] | None = None
|
|
23
|
+
help: str = ""
|
|
24
|
+
cli_arg: str | None = None
|
|
25
|
+
required: bool = False
|
|
26
|
+
is_cli: bool = False
|
|
27
|
+
category: str = "general"
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
if self.is_cli and self.cli_arg is None and not self.required:
|
|
31
|
+
self.cli_arg = f"--{self.name}"
|
|
32
|
+
if isinstance(self.value, bool) and self.choices is None:
|
|
33
|
+
self.choices = [True, False]
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def type_(self) -> type[Any]:
|
|
37
|
+
"""Return the Python type of this parameter’s value."""
|
|
38
|
+
return type(self.value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConfigCategory(BaseModel, ABC):
|
|
42
|
+
"""Base class for configuration categories."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_category_name(self) -> str:
|
|
46
|
+
"""Return the unique name for this configuration category."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def get_parameters(self) -> list[ConfigParameter]:
|
|
50
|
+
"""Return all `ConfigParameter` instances from this category."""
|
|
51
|
+
params = []
|
|
52
|
+
for value in vars(self).values(): # faster, only instance attrs
|
|
53
|
+
if isinstance(value, ConfigParameter):
|
|
54
|
+
value.category = self.get_category_name()
|
|
55
|
+
params.append(value)
|
|
56
|
+
return params
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConfigSerializer:
|
|
60
|
+
"""Handles serialization and deserialization of custom config types."""
|
|
61
|
+
|
|
62
|
+
TYPE_MAPPING = {
|
|
63
|
+
Font: {
|
|
64
|
+
"to_serializable": lambda v: v.to_str(),
|
|
65
|
+
"from_serializable": lambda v: Font.from_list(v)
|
|
66
|
+
if isinstance(v, list)
|
|
67
|
+
else Font.from_str(v),
|
|
68
|
+
},
|
|
69
|
+
Color: {
|
|
70
|
+
"to_serializable": lambda v: v.to_hex(),
|
|
71
|
+
"from_serializable": lambda v: Color.from_list(v)
|
|
72
|
+
if isinstance(v, list)
|
|
73
|
+
else (Color.from_hex(v) if isinstance(v, str) else v),
|
|
74
|
+
},
|
|
75
|
+
Vector: {
|
|
76
|
+
"to_serializable": lambda v: v.to_str(),
|
|
77
|
+
"from_serializable": lambda v: Vector.from_list(v)
|
|
78
|
+
if isinstance(v, list)
|
|
79
|
+
else (Vector.from_str(v) if isinstance(v, str) else v),
|
|
80
|
+
},
|
|
81
|
+
Path: {
|
|
82
|
+
"to_serializable": lambda v: str(v.as_posix()),
|
|
83
|
+
"from_serializable": lambda v: Path(v) if isinstance(v, str) else v,
|
|
84
|
+
},
|
|
85
|
+
datetime: {
|
|
86
|
+
"to_serializable": lambda v: v.isoformat(),
|
|
87
|
+
"from_serializable": lambda v: datetime.fromisoformat(v) if isinstance(v, str) else v,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def to_serializable(self, value: Any) -> Any:
|
|
92
|
+
"""Convert a value to a serializable format."""
|
|
93
|
+
for type_class, methods in self.TYPE_MAPPING.items():
|
|
94
|
+
if isinstance(value, type_class):
|
|
95
|
+
return methods["to_serializable"](value)
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
def from_serializable(self, value: Any, target_type: type[Any]) -> Any:
|
|
99
|
+
"""Convert a value from a serializable format to its original type."""
|
|
100
|
+
for type_class, methods in self.TYPE_MAPPING.items():
|
|
101
|
+
if target_type == type_class:
|
|
102
|
+
return methods["from_serializable"](value)
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ConfigManager:
|
|
107
|
+
"""Manages loading, saving, and accessing configuration categories."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
categories: tuple[ConfigCategory, ...],
|
|
112
|
+
config_file: str | None = None,
|
|
113
|
+
**overrides: Any,
|
|
114
|
+
):
|
|
115
|
+
self._categories: dict[str, ConfigCategory] = {}
|
|
116
|
+
self._serializer = ConfigSerializer()
|
|
117
|
+
|
|
118
|
+
for category in categories:
|
|
119
|
+
if not isinstance(category, ConfigCategory):
|
|
120
|
+
raise TypeError(f"Expected ConfigCategory instance, got {type(category)}")
|
|
121
|
+
self.add_category(category.get_category_name(), category)
|
|
122
|
+
|
|
123
|
+
if config_file:
|
|
124
|
+
self.load_from_file(config_file)
|
|
125
|
+
|
|
126
|
+
self.apply_overrides(overrides)
|
|
127
|
+
|
|
128
|
+
def add_category(self, name: str, category: ConfigCategory) -> None:
|
|
129
|
+
"""Register a new configuration category."""
|
|
130
|
+
self._categories[name] = category
|
|
131
|
+
setattr(self, name, category)
|
|
132
|
+
|
|
133
|
+
def get_category(self, name: str) -> ConfigCategory | None:
|
|
134
|
+
"""Retrieve a category by name."""
|
|
135
|
+
return self._categories.get(name)
|
|
136
|
+
|
|
137
|
+
def apply_overrides(self, overrides: dict[str, Any]) -> None:
|
|
138
|
+
"""Apply keyword overrides in the format `category__param=value`."""
|
|
139
|
+
for key, value in overrides.items():
|
|
140
|
+
if "__" not in key:
|
|
141
|
+
continue
|
|
142
|
+
category_name, param_name = key.split("__", 1)
|
|
143
|
+
category = self._categories.get(category_name)
|
|
144
|
+
if category and hasattr(category, param_name):
|
|
145
|
+
param = getattr(category, param_name)
|
|
146
|
+
if isinstance(param, ConfigParameter):
|
|
147
|
+
param.value = value
|
|
148
|
+
else:
|
|
149
|
+
setattr(category, param_name, value)
|
|
150
|
+
|
|
151
|
+
def load_from_file(self, config_file: str) -> None:
|
|
152
|
+
"""Load configuration from a YAML or JSON file."""
|
|
153
|
+
path = Path(config_file)
|
|
154
|
+
if not path.exists():
|
|
155
|
+
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
156
|
+
|
|
157
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
158
|
+
data = yaml.safe_load(f) if path.suffix.lower() in [".yml", ".yaml"] else json.load(f)
|
|
159
|
+
|
|
160
|
+
self._apply_config_data(data)
|
|
161
|
+
|
|
162
|
+
def _apply_config_data(self, data: dict[str, Any]) -> None:
|
|
163
|
+
"""Apply loaded data to the configuration parameters."""
|
|
164
|
+
for category_name, category_data in data.items():
|
|
165
|
+
category = self._categories.get(category_name)
|
|
166
|
+
if not category:
|
|
167
|
+
continue
|
|
168
|
+
for param_name, param_value in category_data.items():
|
|
169
|
+
if hasattr(category, param_name):
|
|
170
|
+
param: ConfigParameter = getattr(category, param_name)
|
|
171
|
+
if isinstance(param, ConfigParameter):
|
|
172
|
+
target_type = param.type_
|
|
173
|
+
if isinstance(param.value, Path):
|
|
174
|
+
target_type = Path
|
|
175
|
+
deserialized_value = self._serializer.from_serializable(
|
|
176
|
+
param_value, target_type
|
|
177
|
+
)
|
|
178
|
+
param.value = deserialized_value
|
|
179
|
+
|
|
180
|
+
def save_to_file(self, config_file: str, format_: str = "auto") -> None:
|
|
181
|
+
"""Save the current configuration to a file."""
|
|
182
|
+
path = Path(config_file)
|
|
183
|
+
data = self.to_dict()
|
|
184
|
+
|
|
185
|
+
file_format = format_
|
|
186
|
+
if file_format == "auto":
|
|
187
|
+
file_format = "yaml" if path.suffix.lower() in [".yml", ".yaml"] else "json"
|
|
188
|
+
|
|
189
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
191
|
+
if file_format == "yaml":
|
|
192
|
+
yaml.dump(data, f, indent=2, sort_keys=False)
|
|
193
|
+
else:
|
|
194
|
+
json.dump(data, f, indent=2)
|
|
195
|
+
|
|
196
|
+
if file_format == "yaml":
|
|
197
|
+
self._append_comments_to_yaml(path)
|
|
198
|
+
|
|
199
|
+
def to_dict(self) -> dict[str, Any]:
|
|
200
|
+
"""Convert all configuration categories to a dictionary."""
|
|
201
|
+
result: dict[str, dict[str, Any]] = {}
|
|
202
|
+
for category_name, category in self._categories.items():
|
|
203
|
+
result[category_name] = {}
|
|
204
|
+
for param in category.get_parameters():
|
|
205
|
+
value = self._serializer.to_serializable(param.value)
|
|
206
|
+
result[category_name][param.name] = value
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
def get_all_parameters(self) -> list[ConfigParameter]:
|
|
210
|
+
"""Return a flat list of all parameters from all categories."""
|
|
211
|
+
return [p for c in self._categories.values() for p in c.get_parameters()]
|
|
212
|
+
|
|
213
|
+
def get_cli_parameters(self) -> list[ConfigParameter]:
|
|
214
|
+
"""Return a list of all parameters that are exposed to the CLI."""
|
|
215
|
+
return [p for p in self.get_all_parameters() if p.is_cli]
|
|
216
|
+
|
|
217
|
+
def _append_comments_to_yaml(self, path: Path) -> None:
|
|
218
|
+
"""Append helpful metadata comments to the YAML file."""
|
|
219
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
220
|
+
new_lines = []
|
|
221
|
+
all_params = {p.name: p for p in self.get_all_parameters()}
|
|
222
|
+
current_category = ""
|
|
223
|
+
|
|
224
|
+
for line in lines:
|
|
225
|
+
stripped = line.strip()
|
|
226
|
+
if (
|
|
227
|
+
stripped.endswith(":")
|
|
228
|
+
and not stripped.startswith("#")
|
|
229
|
+
and line.lstrip() == stripped
|
|
230
|
+
):
|
|
231
|
+
current_category = stripped[:-1]
|
|
232
|
+
new_lines.append(line)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
param_name = stripped.split(":")[0].strip()
|
|
236
|
+
if param_name in all_params:
|
|
237
|
+
param = all_params[param_name]
|
|
238
|
+
if param.category == current_category:
|
|
239
|
+
indent = " " * (line.find(param_name))
|
|
240
|
+
comment_parts = [param.help, f"type={param.type_.__name__}"]
|
|
241
|
+
if param.is_cli:
|
|
242
|
+
comment_parts.append("[CLI]")
|
|
243
|
+
if param.choices:
|
|
244
|
+
comment_parts.append(f"choices={param.choices}")
|
|
245
|
+
|
|
246
|
+
comment = f"{indent}# " + " | ".join(filter(None, comment_parts))
|
|
247
|
+
new_lines.append(comment)
|
|
248
|
+
|
|
249
|
+
new_lines.append(line)
|
|
250
|
+
|
|
251
|
+
path.write_text("\n".join(new_lines), encoding="utf-8")
|
|
@@ -54,6 +54,40 @@ class Font:
|
|
|
54
54
|
)
|
|
55
55
|
return cls(str(font_type), float(size), color)
|
|
56
56
|
|
|
57
|
+
def to_str(self) -> str:
|
|
58
|
+
return f"{self.name}, {self.size}, {self.color.to_hex()}"
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_str(cls, font_init_str: str) -> "Font":
|
|
62
|
+
"""
|
|
63
|
+
Parses a string of the form:
|
|
64
|
+
"FontName, size, #rrggbb"
|
|
65
|
+
or alternative color formats.
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(font_init_str, str):
|
|
68
|
+
return cls("Arial", 12, Color(0, 0, 0))
|
|
69
|
+
|
|
70
|
+
parts = [p.strip() for p in font_init_str.split(",")]
|
|
71
|
+
|
|
72
|
+
# at least name, size, color
|
|
73
|
+
if len(parts) < 3:
|
|
74
|
+
return cls("Arial", 12, Color(0, 0, 0))
|
|
75
|
+
|
|
76
|
+
# parse font name
|
|
77
|
+
font_name = parts[0]
|
|
78
|
+
|
|
79
|
+
# parse size
|
|
80
|
+
try:
|
|
81
|
+
size = float(parts[1])
|
|
82
|
+
except ValueError:
|
|
83
|
+
size = 12.0
|
|
84
|
+
|
|
85
|
+
# parse color
|
|
86
|
+
color_raw = ",".join(parts[2:]).strip()
|
|
87
|
+
color = Color.from_hex(color_raw)
|
|
88
|
+
|
|
89
|
+
return cls(font_name, size, color)
|
|
90
|
+
|
|
57
91
|
def get_image_font(self, dpi=25.4) -> ImageFont.FreeTypeFont:
|
|
58
92
|
"""
|
|
59
93
|
Return a PIL FreeTypeFont, with fallback to default.
|
|
@@ -77,4 +111,4 @@ class Font:
|
|
|
77
111
|
return f"Font(type='{self.name}', size={self.size}, color={self.color!r})"
|
|
78
112
|
|
|
79
113
|
def __str__(self) -> str:
|
|
80
|
-
return f"{self.
|
|
114
|
+
return f"{self.to_str()}"
|