config-cli-gui 0.1.9__tar.gz → 0.2.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.1.9 → config_cli_gui-0.2.0}/HISTORY.md +17 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/PKG-INFO +1 -1
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/README.md +35 -24
- config_cli_gui-0.2.0/config.yaml +48 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/usage/config.md +1 -1
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/_version.py +3 -3
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/cli.py +1 -1
- config_cli_gui-0.2.0/src/config_cli_gui/config.py +245 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/gui.py +17 -16
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/PKG-INFO +1 -1
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/SOURCES.txt +2 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/core/logging.py +6 -6
- config_cli_gui-0.2.0/tests/example_project/gui/config.yaml +48 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/gui/gui_example.py +6 -6
- config_cli_gui-0.2.0/tests/test_config_manager.py +203 -0
- config_cli_gui-0.2.0/tests/test_docs.py +230 -0
- config_cli_gui-0.1.9/config.yaml +0 -51
- config_cli_gui-0.1.9/src/config_cli_gui/config.py +0 -245
- config_cli_gui-0.1.9/tests/example_project/gui/config.yaml +0 -51
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/FUNDING.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/actions/setup-environment/action.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/dependabot.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/init.sh +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/release_message.sh +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/update_funding.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/workflows/main.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/workflows/release.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/workflows/update_readme.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.gitignore +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.idea/runConfigurations/config_generate.xml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.pre-commit-config.yaml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.readthedocs.yaml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/LICENSE +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/Makefile +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/.nav.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/_static/img/favicon.png +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/_static/img/logo.png +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/css/custom.css +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/develop/contributing.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/develop/make_windows.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/develop/naming_convention.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/funding/funding.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/getting-started/install.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/getting-started/virtual-environment.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/index.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/usage/cli.md +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/mkdocs.yml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/pyproject.toml +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/show_filelist.ps1 +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/show_tree.ps1 +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/show_tree.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/update_readme.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/setup.cfg +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/docs.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/dependency_links.txt +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/entry_points.txt +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/requires.txt +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/top_level.txt +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/template.yml.url +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/__main__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/cli/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/cli/__main__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/cli/cli_example.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/config/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/config/config_example.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/core/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/core/base.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/gui/__init__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/gui/__main__.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/test_generic_cli.py +0 -0
- {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/uv.lock +0 -0
|
@@ -4,6 +4,23 @@ Changelog
|
|
|
4
4
|
|
|
5
5
|
(unreleased)
|
|
6
6
|
------------
|
|
7
|
+
- #9 fix posix path issue. [Paul Magister]
|
|
8
|
+
- #9 fix posix path issue. [Paul Magister]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
0.1.10 (2025-11-13)
|
|
12
|
+
-------------------
|
|
13
|
+
- Docs: Update HISTORY.md for release 0.1.10. [Paul Magister]
|
|
14
|
+
- #9 unittests docs.py. [Paul Magister]
|
|
15
|
+
- #9 unittests config.py. [Paul Magister]
|
|
16
|
+
- #9 roll back to .value usage to be type save: documentation. [Paul
|
|
17
|
+
Magister]
|
|
18
|
+
- #9 roll back to .value usage to be type save. [Paul Magister]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
0.1.9 (2025-11-12)
|
|
22
|
+
------------------
|
|
23
|
+
- Docs: Update HISTORY.md for release 0.1.9. [Paul Magister]
|
|
7
24
|
- #8 fix bug with .value. [Paul Magister]
|
|
8
25
|
|
|
9
26
|
|
|
@@ -58,11 +58,16 @@ Start by defining your application's configuration parameters in a central `conf
|
|
|
58
58
|
```python
|
|
59
59
|
# my_project/config_example.py
|
|
60
60
|
|
|
61
|
+
from datetime import datetime
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
|
|
61
64
|
from config_cli_gui.config import (
|
|
65
|
+
Color,
|
|
62
66
|
ConfigCategory,
|
|
63
67
|
ConfigManager,
|
|
64
68
|
ConfigParameter,
|
|
65
69
|
)
|
|
70
|
+
from config_cli_gui.docs import DocumentationGenerator
|
|
66
71
|
|
|
67
72
|
|
|
68
73
|
class CliConfig(ConfigCategory):
|
|
@@ -74,55 +79,61 @@ class CliConfig(ConfigCategory):
|
|
|
74
79
|
# Positional argument
|
|
75
80
|
input: ConfigParameter = ConfigParameter(
|
|
76
81
|
name="input",
|
|
77
|
-
|
|
82
|
+
value="",
|
|
78
83
|
help="Path to input (file or folder)",
|
|
79
84
|
required=True,
|
|
80
85
|
is_cli=True,
|
|
81
86
|
)
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
# Optional CLI arguments
|
|
89
|
+
output: ConfigParameter = ConfigParameter(
|
|
90
|
+
name="output",
|
|
91
|
+
value="",
|
|
92
|
+
help="Path to output destination",
|
|
87
93
|
is_cli=True,
|
|
88
94
|
)
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
name="
|
|
92
|
-
|
|
93
|
-
help="
|
|
96
|
+
min_dist: ConfigParameter = ConfigParameter(
|
|
97
|
+
name="min_dist",
|
|
98
|
+
value=20,
|
|
99
|
+
help="Maximum distance between two waypoints",
|
|
94
100
|
is_cli=True,
|
|
95
101
|
)
|
|
96
102
|
|
|
97
103
|
|
|
98
|
-
class
|
|
99
|
-
"""Application-specific configuration parameters."""
|
|
100
|
-
|
|
104
|
+
class MiscConfig(ConfigCategory):
|
|
101
105
|
def get_category_name(self) -> str:
|
|
102
|
-
return "
|
|
106
|
+
return "misc"
|
|
107
|
+
|
|
108
|
+
some_file: ConfigParameter = ConfigParameter(
|
|
109
|
+
name="some_file",
|
|
110
|
+
value=Path("some_file.txt"),
|
|
111
|
+
help="Path to the file to use",
|
|
112
|
+
)
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
name="
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
help="Logging level for the application",
|
|
114
|
+
some_color: ConfigParameter = ConfigParameter(
|
|
115
|
+
name="some_color",
|
|
116
|
+
value=Color(255, 0, 0),
|
|
117
|
+
help="Color setting for the application",
|
|
109
118
|
)
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
name="
|
|
113
|
-
|
|
114
|
-
help="
|
|
120
|
+
some_date: ConfigParameter = ConfigParameter(
|
|
121
|
+
name="some_date",
|
|
122
|
+
value=datetime.now(),
|
|
123
|
+
help="Date setting for the application",
|
|
115
124
|
)
|
|
116
125
|
|
|
117
126
|
|
|
118
127
|
class ProjectConfigManager(ConfigManager): # Inherit from ConfigManager
|
|
119
128
|
"""Main configuration manager that handles all parameter categories."""
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
cli: CliConfig
|
|
131
|
+
misc: MiscConfig
|
|
122
132
|
|
|
123
133
|
def __init__(self, config_file: str | None = None, **kwargs):
|
|
124
134
|
"""Initialize the configuration manager with all parameter categories."""
|
|
125
|
-
|
|
135
|
+
categories = (CliConfig(), MiscConfig())
|
|
136
|
+
super().__init__(categories, config_file, **kwargs)
|
|
126
137
|
|
|
127
138
|
|
|
128
139
|
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
app:
|
|
2
|
+
# Date format to use | type=str, default=%Y-%m-%d
|
|
3
|
+
date_format: '%Y-%m-%d'
|
|
4
|
+
# Enable logging to console | type=bool, default=True
|
|
5
|
+
enable_console_logging: true
|
|
6
|
+
# Enable logging to file | type=bool, default=True
|
|
7
|
+
enable_file_logging: true
|
|
8
|
+
# Number of backup log files to keep | type=int, default=5
|
|
9
|
+
log_backup_count: 5
|
|
10
|
+
# Maximum log file size in MB before rotation | type=int, default=10
|
|
11
|
+
log_file_max_size: 10
|
|
12
|
+
# Log message format style | type=str, default=detailed
|
|
13
|
+
log_format: detailed
|
|
14
|
+
# Logging level for the application | type=str, default=INFO
|
|
15
|
+
log_level: INFO
|
|
16
|
+
# Maximum number of worker threads | type=int, default=4
|
|
17
|
+
max_workers: 4
|
|
18
|
+
cli:
|
|
19
|
+
# Include elevation data in waypoints | type=bool, default=True [CLI]
|
|
20
|
+
elevation: true
|
|
21
|
+
# Extract starting points of each track as waypoint | type=bool, default=True [CLI]
|
|
22
|
+
extract_waypoints: true
|
|
23
|
+
# Path to input (file or folder) | type=str, default= [CLI]
|
|
24
|
+
input: ''
|
|
25
|
+
# Maximum distance between two waypoints | type=int, default=20 [CLI]
|
|
26
|
+
min_dist: 20
|
|
27
|
+
# Path to output destination | type=str, default= [CLI]
|
|
28
|
+
output: ''
|
|
29
|
+
gui:
|
|
30
|
+
# Automatically scroll to the newest log entries | type=bool, default=True
|
|
31
|
+
auto_scroll_log: true
|
|
32
|
+
# Height of the log window in pixels | type=int, default=200
|
|
33
|
+
log_window_height: 200
|
|
34
|
+
# Maximum number of log lines to keep in GUI | type=int, default=1000
|
|
35
|
+
max_log_lines: 1000
|
|
36
|
+
# GUI theme setting | type=str, default=light
|
|
37
|
+
theme: light
|
|
38
|
+
# Default window height | type=int, default=600
|
|
39
|
+
window_height: 600
|
|
40
|
+
# Default window width | type=int, default=800
|
|
41
|
+
window_width: 800
|
|
42
|
+
misc:
|
|
43
|
+
# Color setting for the application | type=Color, default=#ff0000
|
|
44
|
+
some_color: '#ff0000'
|
|
45
|
+
# Date setting for the application | type=datetime, default=2025-11-13 22:49:38.801511
|
|
46
|
+
some_date: '2025-11-13T22:49:38.801511'
|
|
47
|
+
# Path to the file to use | type=PosixPath, default=some_file.txt
|
|
48
|
+
some_file: some_file.txt
|
|
@@ -43,5 +43,5 @@ The parameters in the cli category can be accessed via the command line interfac
|
|
|
43
43
|
|------------|-----------|-----------------------------------|-----------------------------------------------------|---------|
|
|
44
44
|
| some_file | PosixPath | Path to the file to use | PosixPath('some_file.txt') | - |
|
|
45
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, 11,
|
|
46
|
+
| some_date | datetime | Date setting for the application | datetime.datetime(2025, 11, 13, 22, 49, 38, 801511) | - |
|
|
47
47
|
|
|
@@ -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.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gd5e29e239'
|
|
@@ -130,7 +130,7 @@ class CliGenerator:
|
|
|
130
130
|
updated_config.add_category(name, category)
|
|
131
131
|
|
|
132
132
|
# Apply overrides again after copying categories
|
|
133
|
-
updated_config.
|
|
133
|
+
updated_config.apply_overrides(cli_overrides)
|
|
134
134
|
|
|
135
135
|
# Try to get logger if logging is configured
|
|
136
136
|
try:
|
|
@@ -0,0 +1,245 @@
|
|
|
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 | str]) -> "Color":
|
|
28
|
+
if len(rgb_list) >= 3:
|
|
29
|
+
return cls(int(rgb_list[0]), int(rgb_list[1]), int(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(
|
|
37
|
+
int(hex_color[0:2], 16),
|
|
38
|
+
int(hex_color[2:4], 16),
|
|
39
|
+
int(hex_color[4:6], 16),
|
|
40
|
+
)
|
|
41
|
+
return cls()
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return self.to_hex()
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return f"Color({self.r}, {self.g}, {self.b})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ConfigParameter:
|
|
52
|
+
"""Represents a single configuration parameter with metadata."""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
value: Any
|
|
56
|
+
choices: list | tuple | None = None
|
|
57
|
+
help: str = ""
|
|
58
|
+
cli_arg: str | None = None
|
|
59
|
+
required: bool = False
|
|
60
|
+
is_cli: bool = False
|
|
61
|
+
category: str = "general"
|
|
62
|
+
|
|
63
|
+
def __post_init__(self):
|
|
64
|
+
if self.is_cli and self.cli_arg is None and not self.required:
|
|
65
|
+
self.cli_arg = f"--{self.name}"
|
|
66
|
+
if isinstance(self.value, bool) and self.choices is None:
|
|
67
|
+
self.choices = [True, False]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def type_(self) -> type:
|
|
71
|
+
"""Return the Python type of this parameter’s value."""
|
|
72
|
+
return type(self.value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConfigCategory(BaseModel, ABC):
|
|
76
|
+
"""Base class for configuration categories."""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def get_category_name(self) -> str:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def get_parameters(self) -> list[ConfigParameter]:
|
|
83
|
+
"""Return ConfigParameter instances that are actual instance attributes."""
|
|
84
|
+
params = []
|
|
85
|
+
|
|
86
|
+
for value in vars(self).values(): # faster, only instance attrs
|
|
87
|
+
if isinstance(value, ConfigParameter):
|
|
88
|
+
value.category = self.get_category_name()
|
|
89
|
+
params.append(value)
|
|
90
|
+
|
|
91
|
+
return params
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ConfigManager:
|
|
95
|
+
"""Generic configuration manager handling multiple configuration categories."""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
categories: tuple[ConfigCategory, ...],
|
|
100
|
+
config_file: str | None = None,
|
|
101
|
+
**overrides: Any,
|
|
102
|
+
):
|
|
103
|
+
self._categories: dict[str, ConfigCategory] = {}
|
|
104
|
+
|
|
105
|
+
# Register categories and expose them as attributes
|
|
106
|
+
for category in categories:
|
|
107
|
+
if not isinstance(category, ConfigCategory):
|
|
108
|
+
raise TypeError(f"Expected ConfigCategory instance, got {type(category)}")
|
|
109
|
+
self.add_category(category.get_category_name(), category)
|
|
110
|
+
|
|
111
|
+
# Load configuration from file if provided
|
|
112
|
+
if config_file:
|
|
113
|
+
self.load_from_file(config_file)
|
|
114
|
+
|
|
115
|
+
# Apply overrides (category__param=value)
|
|
116
|
+
self.apply_overrides(overrides)
|
|
117
|
+
|
|
118
|
+
def add_category(self, name: str, category: ConfigCategory):
|
|
119
|
+
self._categories[name] = category
|
|
120
|
+
setattr(self, name, category)
|
|
121
|
+
|
|
122
|
+
def get_category(self, name: str) -> ConfigCategory | None:
|
|
123
|
+
return self._categories.get(name)
|
|
124
|
+
|
|
125
|
+
def apply_overrides(self, overrides: dict[str, Any]):
|
|
126
|
+
"""Apply keyword overrides in format category__param=value."""
|
|
127
|
+
for key, value in overrides.items():
|
|
128
|
+
if "__" not in key:
|
|
129
|
+
continue
|
|
130
|
+
category_name, param_name = key.split("__", 1)
|
|
131
|
+
category = self._categories.get(category_name)
|
|
132
|
+
if category and hasattr(category, param_name):
|
|
133
|
+
param = getattr(category, param_name)
|
|
134
|
+
if isinstance(param, ConfigParameter):
|
|
135
|
+
param.value = value
|
|
136
|
+
else:
|
|
137
|
+
setattr(category, param_name, value)
|
|
138
|
+
|
|
139
|
+
def load_from_file(self, config_file: str):
|
|
140
|
+
path = Path(config_file)
|
|
141
|
+
if not path.exists():
|
|
142
|
+
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
143
|
+
|
|
144
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
145
|
+
data = yaml.safe_load(f) if path.suffix in [".yml", ".yaml"] else json.load(f)
|
|
146
|
+
|
|
147
|
+
self._apply_config_data(data)
|
|
148
|
+
|
|
149
|
+
def _apply_config_data(self, data: dict):
|
|
150
|
+
for category_name, category_data in data.items():
|
|
151
|
+
category = self._categories.get(category_name)
|
|
152
|
+
if not category:
|
|
153
|
+
continue
|
|
154
|
+
for param_name, param_value in category_data.items():
|
|
155
|
+
param: ConfigParameter = getattr(category, param_name, None)
|
|
156
|
+
if not isinstance(param, ConfigParameter):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Type conversions
|
|
160
|
+
if isinstance(param.value, Color) and isinstance(param_value, list):
|
|
161
|
+
param_value = Color.from_list(param_value)
|
|
162
|
+
if isinstance(param.value, Color) and isinstance(param_value, str):
|
|
163
|
+
param_value = Color.from_hex(param_value)
|
|
164
|
+
elif isinstance(param.value, Path):
|
|
165
|
+
param_value = Path(param_value)
|
|
166
|
+
elif isinstance(param.value, datetime):
|
|
167
|
+
param_value = datetime.fromisoformat(param_value)
|
|
168
|
+
|
|
169
|
+
param.value = param_value
|
|
170
|
+
|
|
171
|
+
def save_to_file(self, config_file: str, format_: str = "auto"):
|
|
172
|
+
path = Path(config_file)
|
|
173
|
+
data = self.to_dict()
|
|
174
|
+
|
|
175
|
+
if format_ == "auto":
|
|
176
|
+
format_ = "yaml" if path.suffix in [".yml", ".yaml"] else "json"
|
|
177
|
+
|
|
178
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
180
|
+
if format_ == "yaml":
|
|
181
|
+
yaml.dump(data, f, indent=2)
|
|
182
|
+
else:
|
|
183
|
+
json.dump(data, f, indent=2)
|
|
184
|
+
|
|
185
|
+
if format_ == "yaml":
|
|
186
|
+
self._append_comments_to_yaml(path)
|
|
187
|
+
|
|
188
|
+
def to_dict(self) -> dict[str, Any]:
|
|
189
|
+
"""Convert all configuration categories to a dictionary of plain values."""
|
|
190
|
+
result: dict[str, dict[str, Any]] = {}
|
|
191
|
+
for category in self._categories.values():
|
|
192
|
+
category_name = category.get_category_name()
|
|
193
|
+
result[category_name] = {}
|
|
194
|
+
for param in category.get_parameters():
|
|
195
|
+
val = getattr(category, param.name).value
|
|
196
|
+
if isinstance(val, Color):
|
|
197
|
+
val = val.to_hex()
|
|
198
|
+
elif isinstance(val, Path):
|
|
199
|
+
val = str(val.as_posix())
|
|
200
|
+
elif isinstance(val, datetime):
|
|
201
|
+
val = val.isoformat()
|
|
202
|
+
result[category_name][param.name] = val
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
def get_all_parameters(self) -> list[ConfigParameter]:
|
|
206
|
+
return [p for c in self._categories.values() for p in c.get_parameters()]
|
|
207
|
+
|
|
208
|
+
def get_cli_parameters(self) -> list[ConfigParameter]:
|
|
209
|
+
return [p for p in self.get_all_parameters() if p.is_cli]
|
|
210
|
+
|
|
211
|
+
def _append_comments_to_yaml(self, path: Path):
|
|
212
|
+
"""Append helpful metadata comments to the YAML file."""
|
|
213
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
214
|
+
new_lines = []
|
|
215
|
+
all_params = {p.name: p for p in self.get_all_parameters()}
|
|
216
|
+
current_category = None
|
|
217
|
+
|
|
218
|
+
for line in lines:
|
|
219
|
+
stripped = line.strip()
|
|
220
|
+
if (
|
|
221
|
+
stripped.endswith(":")
|
|
222
|
+
and not stripped.startswith("#")
|
|
223
|
+
and line.startswith(stripped)
|
|
224
|
+
):
|
|
225
|
+
current_category = stripped[:-1]
|
|
226
|
+
new_lines.append(line)
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
parts = stripped.split(":", 1)
|
|
230
|
+
if len(parts) > 1:
|
|
231
|
+
param_name = parts[0].strip()
|
|
232
|
+
if param_name in all_params:
|
|
233
|
+
param = all_params[param_name]
|
|
234
|
+
if current_category and param.category == current_category:
|
|
235
|
+
indent = " " * (len(line) - len(stripped))
|
|
236
|
+
comment = (
|
|
237
|
+
f"{indent}# {param.help} | "
|
|
238
|
+
f"type={param.type_.__name__}, default={param.value}"
|
|
239
|
+
f"{' [CLI]' if param.is_cli else ''}"
|
|
240
|
+
)
|
|
241
|
+
new_lines.append(comment)
|
|
242
|
+
|
|
243
|
+
new_lines.append(line)
|
|
244
|
+
|
|
245
|
+
path.write_text("\n".join(new_lines), encoding="utf-8")
|
|
@@ -258,18 +258,19 @@ class GenericSettingsDialog:
|
|
|
258
258
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
259
259
|
|
|
260
260
|
# Add parameters
|
|
261
|
-
self._add_category_parameters(scrollable_frame,
|
|
261
|
+
self._add_category_parameters(scrollable_frame, category)
|
|
262
262
|
|
|
263
263
|
canvas.pack(side="left", fill="both", expand=True)
|
|
264
264
|
scrollbar.pack(side="right", fill="y")
|
|
265
265
|
|
|
266
|
-
def _add_category_parameters(self, parent,
|
|
266
|
+
def _add_category_parameters(self, parent, category: ConfigCategory):
|
|
267
267
|
"""Add parameter widgets for a specific category."""
|
|
268
268
|
row = 0
|
|
269
|
+
category_name: str = category.get_category_name()
|
|
269
270
|
parameters = category.get_parameters()
|
|
270
271
|
|
|
271
272
|
for param in parameters:
|
|
272
|
-
param.value = getattr(category, param.name)
|
|
273
|
+
param.value = getattr(category, param.name).value
|
|
273
274
|
if param.required:
|
|
274
275
|
# Skip required parameters as they are not configurable in GUI
|
|
275
276
|
continue
|
|
@@ -391,8 +392,8 @@ class GenericSettingsDialog:
|
|
|
391
392
|
entry = ttk.Entry(frame, textvariable=var, width=10)
|
|
392
393
|
entry.pack(side=tk.LEFT)
|
|
393
394
|
|
|
394
|
-
color_display = tk.Label(frame, width=
|
|
395
|
-
color_display.pack(side=tk.LEFT, padx=(
|
|
395
|
+
color_display = tk.Label(frame, width=8, bg=color_value.to_hex())
|
|
396
|
+
color_display.pack(side=tk.LEFT, padx=(8, 2))
|
|
396
397
|
|
|
397
398
|
def pick_color():
|
|
398
399
|
color = colorchooser.askcolor(color=var.get())
|
|
@@ -491,35 +492,35 @@ class GenericSettingsDialog:
|
|
|
491
492
|
# Parse value based on parameter type
|
|
492
493
|
category_name, param_name = key.split("__", 1)
|
|
493
494
|
category = self.config_manager.get_category(category_name)
|
|
494
|
-
|
|
495
|
+
param_value = getattr(category, param_name).value
|
|
495
496
|
|
|
496
497
|
# Convert value to appropriate type
|
|
497
|
-
if type(
|
|
498
|
+
if type(param_value) == bool:
|
|
498
499
|
overrides[key] = value
|
|
499
|
-
elif type(
|
|
500
|
+
elif type(param_value) == Path:
|
|
500
501
|
overrides[key] = Path(value)
|
|
501
|
-
elif type(
|
|
502
|
+
elif type(param_value) == Color:
|
|
502
503
|
overrides[key] = Color.from_hex(value)
|
|
503
|
-
elif type(
|
|
504
|
+
elif type(param_value) == datetime:
|
|
504
505
|
overrides[key] = datetime.strptime(value, "%Y-%m-%d %H:%M")
|
|
505
|
-
elif type(
|
|
506
|
+
elif type(param_value) in (list, tuple):
|
|
506
507
|
# Parse comma-separated values
|
|
507
508
|
items = [item.strip() for item in value.split(",") if item.strip()]
|
|
508
|
-
overrides[key] = type(
|
|
509
|
-
elif type(
|
|
509
|
+
overrides[key] = type(param_value)(items)
|
|
510
|
+
elif type(param_value) == dict:
|
|
510
511
|
# Parse JSON format
|
|
511
512
|
import json
|
|
512
513
|
|
|
513
514
|
overrides[key] = json.loads(value)
|
|
514
|
-
elif type(
|
|
515
|
+
elif type(param_value) == int:
|
|
515
516
|
overrides[key] = int(value)
|
|
516
|
-
elif type(
|
|
517
|
+
elif type(param_value) == float:
|
|
517
518
|
overrides[key] = float(value)
|
|
518
519
|
else:
|
|
519
520
|
overrides[key] = value
|
|
520
521
|
|
|
521
522
|
# Apply overrides to config manager
|
|
522
|
-
self.config_manager.
|
|
523
|
+
self.config_manager.apply_overrides(overrides)
|
|
523
524
|
|
|
524
525
|
# Save to file
|
|
525
526
|
self.config_manager.save_to_file(self.config_file)
|
|
@@ -54,6 +54,8 @@ src/config_cli_gui.egg-info/entry_points.txt
|
|
|
54
54
|
src/config_cli_gui.egg-info/requires.txt
|
|
55
55
|
src/config_cli_gui.egg-info/top_level.txt
|
|
56
56
|
tests/__init__.py
|
|
57
|
+
tests/test_config_manager.py
|
|
58
|
+
tests/test_docs.py
|
|
57
59
|
tests/test_generic_cli.py
|
|
58
60
|
tests/example_project/__init__.py
|
|
59
61
|
tests/example_project/__main__.py
|
|
@@ -53,7 +53,7 @@ class LoggerManager:
|
|
|
53
53
|
self.logger.handlers.clear()
|
|
54
54
|
|
|
55
55
|
# Set log level from config
|
|
56
|
-
log_level = getattr(logging, self.config.app.log_level.upper())
|
|
56
|
+
log_level = getattr(logging, self.config.app.log_level.value.upper())
|
|
57
57
|
self.logger.setLevel(log_level)
|
|
58
58
|
|
|
59
59
|
# Create formatters
|
|
@@ -142,15 +142,15 @@ class LoggerManager:
|
|
|
142
142
|
self.logger.setLevel(log_level)
|
|
143
143
|
|
|
144
144
|
# Update config
|
|
145
|
-
self.config.
|
|
145
|
+
self.config.app.log_level.value = level.upper()
|
|
146
146
|
|
|
147
147
|
def log_config_summary(self):
|
|
148
148
|
"""Log current configuration summary."""
|
|
149
149
|
self.logger.info("=== Configuration Summary ===")
|
|
150
|
-
self.logger.info(f"Log level: {self.config.app.log_level}")
|
|
151
|
-
self.logger.info(f"Input: {self.config.cli.input}")
|
|
152
|
-
self.logger.info(f"Output: {self.config.
|
|
153
|
-
self.logger.info(f"Max workers: {self.config.app.max_workers}")
|
|
150
|
+
self.logger.info(f"Log level: {self.config.app.log_level.value}")
|
|
151
|
+
self.logger.info(f"Input: {self.config.cli.input.value}")
|
|
152
|
+
self.logger.info(f"Output: {self.config.cli.output.value}")
|
|
153
|
+
self.logger.info(f"Max workers: {self.config.app.max_workers.value}")
|
|
154
154
|
self.logger.info("==============================")
|
|
155
155
|
|
|
156
156
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
app:
|
|
2
|
+
# Date format to use | type=str, default=%d-%m-%YYYY
|
|
3
|
+
date_format: '%d-%m-%YYYY'
|
|
4
|
+
# Enable logging to console | type=bool, default=True
|
|
5
|
+
enable_console_logging: true
|
|
6
|
+
# Enable logging to file | type=bool, default=True
|
|
7
|
+
enable_file_logging: true
|
|
8
|
+
# Number of backup log files to keep | type=int, default=50
|
|
9
|
+
log_backup_count: 50
|
|
10
|
+
# Maximum log file size in MB before rotation | type=int, default=10
|
|
11
|
+
log_file_max_size: 10
|
|
12
|
+
# Log message format style | type=str, default=detailed
|
|
13
|
+
log_format: detailed
|
|
14
|
+
# Logging level for the application | type=str, default=INFO
|
|
15
|
+
log_level: INFO
|
|
16
|
+
# Maximum number of worker threads | type=int, default=4
|
|
17
|
+
max_workers: 4
|
|
18
|
+
cli:
|
|
19
|
+
# Include elevation data in waypoints | type=bool, default=True [CLI]
|
|
20
|
+
elevation: true
|
|
21
|
+
# Extract starting points of each track as waypoint | type=bool, default=True [CLI]
|
|
22
|
+
extract_waypoints: true
|
|
23
|
+
# Path to input (file or folder) | type=str, default= [CLI]
|
|
24
|
+
input: ''
|
|
25
|
+
# Maximum distance between two waypoints | type=int, default=2480 [CLI]
|
|
26
|
+
min_dist: 2480
|
|
27
|
+
# Path to output destination | type=str, default=test [CLI]
|
|
28
|
+
output: test
|
|
29
|
+
gui:
|
|
30
|
+
# Automatically scroll to the newest log entries | type=bool, default=True
|
|
31
|
+
auto_scroll_log: true
|
|
32
|
+
# Height of the log window in pixels | type=int, default=200
|
|
33
|
+
log_window_height: 200
|
|
34
|
+
# Maximum number of log lines to keep in GUI | type=int, default=1000
|
|
35
|
+
max_log_lines: 1000
|
|
36
|
+
# GUI theme setting | type=str, default=light
|
|
37
|
+
theme: light
|
|
38
|
+
# Default window height | type=int, default=600
|
|
39
|
+
window_height: 600
|
|
40
|
+
# Default window width | type=int, default=800
|
|
41
|
+
window_width: 800
|
|
42
|
+
misc:
|
|
43
|
+
# Color setting for the application | type=Color, default=#c64800
|
|
44
|
+
some_color: '#c64800'
|
|
45
|
+
# Date setting for the application | type=datetime, default=2025-11-12 17:10:00
|
|
46
|
+
some_date: '2025-11-12T17:10:00'
|
|
47
|
+
# Path to the file to use | type=str, default=some_file.txt
|
|
48
|
+
some_file: some_file.txt
|
|
@@ -241,7 +241,7 @@ class MainGui:
|
|
|
241
241
|
|
|
242
242
|
# Log level selector
|
|
243
243
|
ttk.Label(log_controls, text="Log Level:").pack(side=tk.LEFT, padx=(10, 5))
|
|
244
|
-
self.log_level_var = tk.StringVar(value=self._config.app.log_level)
|
|
244
|
+
self.log_level_var = tk.StringVar(value=self._config.app.log_level.value)
|
|
245
245
|
log_level_combo = ttk.Combobox(
|
|
246
246
|
log_controls,
|
|
247
247
|
textvariable=self.log_level_var,
|
|
@@ -454,10 +454,10 @@ class MainGui:
|
|
|
454
454
|
# Create and run project
|
|
455
455
|
project = BaseGPXProcessor(
|
|
456
456
|
files_to_process, # Pass selected files
|
|
457
|
-
self._config.cli.output,
|
|
458
|
-
self._config.cli.min_dist,
|
|
459
|
-
self._config.app.date_format,
|
|
460
|
-
self._config.cli.elevation,
|
|
457
|
+
self._config.cli.output.value,
|
|
458
|
+
self._config.cli.min_dist.value,
|
|
459
|
+
self._config.app.date_format.value,
|
|
460
|
+
self._config.cli.elevation.value,
|
|
461
461
|
self.logger,
|
|
462
462
|
)
|
|
463
463
|
# implement switch case for different processing modes
|
|
@@ -504,7 +504,7 @@ class MainGui:
|
|
|
504
504
|
if dialog.result == "ok":
|
|
505
505
|
self.logger.info("Settings updated successfully")
|
|
506
506
|
# Update log level selector if it changed
|
|
507
|
-
self.log_level_var.set(self._config.app.log_level)
|
|
507
|
+
self.log_level_var.set(self._config.app.log_level.value)
|
|
508
508
|
|
|
509
509
|
def _open_help(self):
|
|
510
510
|
"""Open help documentation in browser."""
|