config-cli-gui 0.0.2__py3-none-any.whl

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.
__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.0.2'
21
+ __version_tuple__ = version_tuple = (0, 0, 2)
@@ -0,0 +1,177 @@
1
+ # config_cli_gui/cli.py
2
+ """Generic CLI generator for configuration framework."""
3
+
4
+ import argparse
5
+ import traceback
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from config_cli_gui.config_framework import ConfigManager
10
+
11
+
12
+ class CliGenerator:
13
+ """Generates CLI interface from ConfigManager."""
14
+
15
+ def __init__(self, config_manager: ConfigManager, app_name: str = "app"):
16
+ self.config_manager = config_manager
17
+ self.app_name = app_name
18
+
19
+ def create_argument_parser(self, description: str = None) -> argparse.ArgumentParser:
20
+ """Create argument parser from configuration."""
21
+ if description is None:
22
+ description = f"Command line interface for {self.app_name}"
23
+
24
+ parser = argparse.ArgumentParser(
25
+ description=description,
26
+ formatter_class=argparse.RawDescriptionHelpFormatter,
27
+ )
28
+
29
+ # Config file argument
30
+ parser.add_argument(
31
+ "--config",
32
+ default=None,
33
+ help="Path to configuration file (JSON or YAML)",
34
+ )
35
+
36
+ # Verbose/quiet options for log level override
37
+ parser.add_argument(
38
+ "-v", "--verbose", action="store_true", help="Enable verbose logging (DEBUG level)"
39
+ )
40
+ parser.add_argument(
41
+ "-q", "--quiet", action="store_true", help="Enable quiet mode (WARNING level only)"
42
+ )
43
+
44
+ # Get CLI parameters
45
+ cli_params = self.config_manager.get_cli_parameters()
46
+
47
+ # Generate arguments from CLI config parameters
48
+ for param in cli_params:
49
+ if param.required and param.cli_arg is None:
50
+ # Positional argument
51
+ parser.add_argument(param.name, help=param.help)
52
+ else:
53
+ # Optional argument
54
+ kwargs = {
55
+ "default": argparse.SUPPRESS,
56
+ "help": f"{param.help} (default: {param.default})",
57
+ }
58
+
59
+ # Handle different parameter types
60
+ if param.choices and param.type_ != bool:
61
+ kwargs["choices"] = param.choices
62
+
63
+ if param.type_ == int:
64
+ kwargs["type"] = int
65
+ elif param.type_ == float:
66
+ kwargs["type"] = float
67
+ elif param.type_ == bool:
68
+ kwargs["action"] = "store_true" if not param.default else "store_false"
69
+ kwargs["help"] = f"{param.help} (default: {param.default})"
70
+ elif param.type_ == str:
71
+ kwargs["type"] = str
72
+
73
+ parser.add_argument(param.cli_arg, **kwargs)
74
+
75
+ return parser
76
+
77
+ def create_config_overrides(self, args: argparse.Namespace) -> dict[str, Any]:
78
+ """Create configuration overrides from CLI arguments."""
79
+ cli_params = self.config_manager.get_cli_parameters()
80
+ overrides = {}
81
+
82
+ for param in cli_params:
83
+ if hasattr(args, param.name):
84
+ arg_value = getattr(args, param.name)
85
+ # Add CLI category prefix for override system
86
+ overrides[f"cli__{param.name}"] = arg_value
87
+
88
+ # Handle log level overrides from verbose/quiet flags
89
+ if hasattr(args, "verbose") and args.verbose:
90
+ overrides["app__log_level"] = "DEBUG"
91
+ elif hasattr(args, "quiet") and args.quiet:
92
+ overrides["app__log_level"] = "WARNING"
93
+
94
+ return overrides
95
+
96
+ def run_cli(
97
+ self,
98
+ main_function: Callable[[ConfigManager], int],
99
+ description: str = None,
100
+ validator: Callable[[ConfigManager], bool] = None,
101
+ ) -> int:
102
+ """Run the CLI application with error handling.
103
+
104
+ Args:
105
+ main_function: Function that takes ConfigManager and returns exit code
106
+ description: CLI description
107
+ validator: Optional function to validate configuration
108
+
109
+ Returns:
110
+ Exit code
111
+ """
112
+ logger = None
113
+
114
+ try:
115
+ # Parse command line arguments
116
+ parser = self.create_argument_parser(description)
117
+ args = parser.parse_args()
118
+
119
+ # Create configuration overrides from CLI arguments
120
+ cli_overrides = self.create_config_overrides(args)
121
+
122
+ # Create new config manager with overrides
123
+ config_file = args.config if hasattr(args, "config") and args.config else None
124
+ updated_config = ConfigManager(config_file=config_file, **cli_overrides)
125
+
126
+ # Copy categories from original config manager
127
+ for name, category in self.config_manager._categories.items():
128
+ updated_config.add_category(name, category)
129
+
130
+ # Apply overrides again after copying categories
131
+ updated_config._apply_kwargs(cli_overrides)
132
+
133
+ # Try to get logger if logging is configured
134
+ try:
135
+ from .logging import get_logger
136
+
137
+ logger = get_logger(f"{self.app_name}.cli")
138
+ logger.info(f"Starting {self.app_name} CLI")
139
+ logger.debug(f"Command line arguments: {vars(args)}")
140
+ except ImportError:
141
+ pass
142
+
143
+ # Validate configuration if validator provided
144
+ if validator and not validator(updated_config):
145
+ if logger:
146
+ logger.error("Configuration validation failed")
147
+ else:
148
+ print("Configuration validation failed")
149
+ return 1
150
+
151
+ # Run main function
152
+ return main_function(updated_config)
153
+
154
+ except FileNotFoundError as e:
155
+ if logger:
156
+ logger.error(f"File not found: {e}")
157
+ logger.debug("Full traceback:", exc_info=True)
158
+ else:
159
+ print(f"Error: {e}")
160
+ traceback.print_exc()
161
+ return 1
162
+
163
+ except KeyboardInterrupt:
164
+ if logger:
165
+ logger.warning("Process interrupted by user")
166
+ else:
167
+ print("Process interrupted by user")
168
+ return 130
169
+
170
+ except Exception as e:
171
+ if logger:
172
+ logger.error(f"Unexpected error: {e}")
173
+ logger.debug("Full traceback:", exc_info=True)
174
+ else:
175
+ print(f"Unexpected error: {e}")
176
+ traceback.print_exc()
177
+ return 1
@@ -0,0 +1,362 @@
1
+ # config_framework/core.py
2
+ """Generic configuration framework for Python applications."""
3
+
4
+ import json
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from textwrap import dedent
9
+ from typing import Any
10
+
11
+ import yaml
12
+ from pydantic import BaseModel
13
+
14
+
15
+ @dataclass
16
+ class ConfigParameter:
17
+ """Represents a single configuration parameter with all its metadata."""
18
+
19
+ name: str
20
+ default: Any
21
+ type_: type
22
+ choices: list[str | bool] = None
23
+ help: str = ""
24
+ cli_arg: str = None
25
+ required: bool = False
26
+ category: str = "general"
27
+
28
+ def __post_init__(self):
29
+ if self.cli_arg is None and not self.required:
30
+ self.cli_arg = f"--{self.name}"
31
+ if self.type_ is bool and self.choices is None:
32
+ self.choices = [True, False]
33
+
34
+
35
+ class BaseConfigCategory(BaseModel, ABC):
36
+ """Base class for configuration categories."""
37
+
38
+ @abstractmethod
39
+ def get_category_name(self) -> str:
40
+ """Return the category name for this configuration group."""
41
+ pass
42
+
43
+ def get_parameters(self) -> list[ConfigParameter]:
44
+ """Get all ConfigParameter objects from this category."""
45
+ parameters = []
46
+ for field_name in self.model_fields:
47
+ param = getattr(self, field_name)
48
+ if isinstance(param, ConfigParameter):
49
+ param.category = self.get_category_name()
50
+ parameters.append(param)
51
+ return parameters
52
+
53
+
54
+ class CliConfigCategory(BaseConfigCategory):
55
+ """Base class for CLI-specific configuration parameters."""
56
+
57
+ def get_category_name(self) -> str:
58
+ return "cli"
59
+
60
+
61
+ class ConfigManager:
62
+ """Generic configuration manager that can handle multiple configuration categories."""
63
+
64
+ def __init__(self, config_file: str = None, **kwargs):
65
+ """Initialize configuration manager.
66
+
67
+ Args:
68
+ config_file: Path to configuration file (JSON or YAML)
69
+ **kwargs: Override parameters in format category__parameter
70
+ """
71
+ self._categories: dict[str, BaseConfigCategory] = {}
72
+ self._cli_category_name: str = None
73
+
74
+ # Load from file if provided
75
+ if config_file:
76
+ self.load_from_file(config_file)
77
+
78
+ # Override with provided kwargs
79
+ self._apply_kwargs(kwargs)
80
+
81
+ def add_category(self, name: str, category: BaseConfigCategory):
82
+ """Add a configuration category.
83
+
84
+ Args:
85
+ name: Name of the category (e.g., 'cli', 'app', 'gui')
86
+ category: Configuration category instance
87
+ """
88
+ self._categories[name] = category
89
+ if isinstance(category, CliConfigCategory):
90
+ self._cli_category_name = name
91
+
92
+ def get_category(self, name: str) -> BaseConfigCategory:
93
+ """Get a configuration category by name."""
94
+ return self._categories.get(name)
95
+
96
+ def get_cli_category(self) -> BaseConfigCategory | None:
97
+ """Get the CLI configuration category."""
98
+ if self._cli_category_name:
99
+ return self._categories[self._cli_category_name]
100
+ return None
101
+
102
+ def _apply_kwargs(self, kwargs: dict[str, Any]):
103
+ """Apply keyword arguments to override configuration values."""
104
+ for key, value in kwargs.items():
105
+ if "__" in key:
106
+ category_name, param_name = key.split("__", 1)
107
+ if category_name in self._categories:
108
+ category = self._categories[category_name]
109
+ if hasattr(category, param_name):
110
+ param = getattr(category, param_name)
111
+ if isinstance(param, ConfigParameter):
112
+ param.default = value
113
+
114
+ def load_from_file(self, config_file: str):
115
+ """Load configuration from JSON or YAML file."""
116
+ config_path = Path(config_file)
117
+ if not config_path.exists():
118
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
119
+
120
+ with open(config_path, "r", encoding="utf-8") as f:
121
+ if config_path.suffix.lower() in [".yml", ".yaml"]:
122
+ config_data = yaml.safe_load(f)
123
+ else:
124
+ config_data = json.load(f)
125
+
126
+ # Apply loaded configuration
127
+ for category_name, category_data in config_data.items():
128
+ if category_name in self._categories:
129
+ category = self._categories[category_name]
130
+ for param_name, param_value in category_data.items():
131
+ if hasattr(category, param_name):
132
+ param = getattr(category, param_name)
133
+ if isinstance(param, ConfigParameter):
134
+ param.default = param_value
135
+
136
+ def save_to_file(self, config_file: str, format_: str = "auto"):
137
+ """Save current configuration to file."""
138
+ config_path = Path(config_file)
139
+ config_data = self.to_dict()
140
+
141
+ # Determine format
142
+ if format_ == "auto":
143
+ format_ = "yaml" if config_path.suffix.lower() in [".yml", ".yaml"] else "json"
144
+
145
+ # Ensure directory exists
146
+ config_path.parent.mkdir(parents=True, exist_ok=True)
147
+
148
+ with open(config_path, "w", encoding="utf-8") as f:
149
+ if format_ == "yaml":
150
+ yaml.dump(config_data, f, default_flow_style=False, indent=2)
151
+ else:
152
+ json.dump(config_data, f, indent=2)
153
+
154
+ def to_dict(self) -> dict[str, Any]:
155
+ """Convert configuration to dictionary."""
156
+ result = {}
157
+ for category_name, category in self._categories.items():
158
+ category_dict = {}
159
+ for param in category.get_parameters():
160
+ category_dict[param.name] = param.default
161
+ result[category_name] = category_dict
162
+ return result
163
+
164
+ def get_all_parameters(self) -> list[ConfigParameter]:
165
+ """Get all parameters from all categories."""
166
+ parameters = []
167
+ for category in self._categories.values():
168
+ parameters.extend(category.get_parameters())
169
+ return parameters
170
+
171
+ def get_cli_parameters(self) -> list[ConfigParameter]:
172
+ """Get only CLI parameters."""
173
+ cli_category = self.get_cli_category()
174
+ if cli_category:
175
+ return cli_category.get_parameters()
176
+ return []
177
+
178
+
179
+ class DocumentationGenerator:
180
+ """Generates documentation and configuration files from ConfigManager."""
181
+
182
+ def __init__(self, config_manager: ConfigManager):
183
+ self.config_manager = config_manager
184
+
185
+ def generate_config_markdown_doc(self, output_file: str):
186
+ """Generate Markdown documentation for all configuration parameters."""
187
+
188
+ def pad(s, width):
189
+ return s + " " * (width - len(s))
190
+
191
+ markdown_content = dedent("""
192
+ # Configuration Parameters
193
+
194
+ These parameters are available to configure the behavior of your application.
195
+ The parameters in the cli category can be accessed via the command line interface.
196
+
197
+ """).lstrip()
198
+
199
+ for category_name, category in self.config_manager._categories.items():
200
+ markdown_content += f'## Category "{category_name}"\n\n'
201
+
202
+ # Collect all parameters for this category
203
+ rows = []
204
+ header = ["Name", "Type", "Description", "Default", "Choices"]
205
+
206
+ for param in category.get_parameters():
207
+ name = param.name
208
+ typ = param.type_.__name__
209
+ desc = param.help
210
+ default = repr(param.default)
211
+ choices = str(param.choices) if param.choices else "-"
212
+
213
+ rows.append((name, typ, desc, default, choices))
214
+
215
+ if not rows:
216
+ continue
217
+
218
+ # Calculate column widths
219
+ all_rows = [header] + rows
220
+ widths = [max(len(str(col)) for col in column) for column in zip(*all_rows)]
221
+
222
+ # Create Markdown table
223
+ table = (
224
+ "| "
225
+ + " | ".join(pad(h, w) for h, w in zip(header, widths))
226
+ + " |\n"
227
+ + "|-"
228
+ + "-|-".join("-" * w for w in widths)
229
+ + "-|\n"
230
+ )
231
+ for row in rows:
232
+ table += "| " + " | ".join(pad(str(col), w) for col, w in zip(row, widths)) + " |\n"
233
+
234
+ markdown_content += table + "\n"
235
+
236
+ # Write to file
237
+ Path(output_file).parent.mkdir(parents=True, exist_ok=True)
238
+ with open(output_file, "w", encoding="utf-8") as f:
239
+ f.write(markdown_content)
240
+
241
+ def generate_default_config_file(self, output_file: str):
242
+ """Generate a default configuration file with all parameters and descriptions."""
243
+ output_path = Path(output_file)
244
+ output_path.parent.mkdir(parents=True, exist_ok=True)
245
+
246
+ with open(output_path, "w", encoding="utf-8") as f:
247
+ f.write("# Configuration File\n")
248
+ f.write("# This file was auto-generated. Modify as needed.\n\n")
249
+
250
+ for category_name, category in self.config_manager._categories.items():
251
+ f.write(f"# {category_name.upper()} Configuration\n")
252
+ f.write(f"{category_name}:\n")
253
+
254
+ for param in category.get_parameters():
255
+ f.write(f" # {param.help}\n")
256
+ if param.choices:
257
+ f.write(f" # Choices: {param.choices}\n")
258
+ f.write(f" # Type: {param.type_.__name__}\n")
259
+ f.write(f" {param.name}: {repr(param.default)}\n\n")
260
+
261
+ f.write("\n")
262
+
263
+ def generate_cli_markdown_doc(self, output_file: str, app_name: str = "app"):
264
+ """Generate Markdown CLI documentation."""
265
+ cli_params = self.config_manager.get_cli_parameters()
266
+
267
+ if not cli_params:
268
+ return
269
+
270
+ rows = []
271
+ required_params = []
272
+ optional_params = []
273
+
274
+ for param in cli_params:
275
+ cli_arg = f"`--{param.name}`" if not param.required else f"`{param.name}`"
276
+ typ = param.type_.__name__
277
+ desc = param.help
278
+ default = (
279
+ "*required*"
280
+ if param.required or param.default in (None, "")
281
+ else repr(param.default)
282
+ )
283
+ choices = str(param.choices) if param.choices else "-"
284
+
285
+ rows.append((cli_arg, typ, desc, default, choices))
286
+ if default == "*required*":
287
+ required_params.append(param)
288
+ else:
289
+ optional_params.append(param)
290
+
291
+ # Generate table
292
+ def pad(s, width):
293
+ return s + " " * (width - len(s))
294
+
295
+ widths = [max(len(str(col)) for col in column) for column in zip(*rows)]
296
+ header = ["Option", "Type", "Description", "Default", "Choices"]
297
+
298
+ table = (
299
+ "| "
300
+ + " | ".join(pad(h, w) for h, w in zip(header, widths))
301
+ + " |\n"
302
+ + "|-"
303
+ + "-|-".join("-" * w for w in widths)
304
+ + "-|\n"
305
+ )
306
+ for row in rows:
307
+ table += "| " + " | ".join(pad(str(col), w) for col, w in zip(row, widths)) + " |\n"
308
+
309
+ # Generate examples
310
+ examples = []
311
+ required_arg = required_params[0].name if required_params else "example.input"
312
+
313
+ examples.append(
314
+ dedent(
315
+ f"""
316
+ ### 1. Basic usage
317
+
318
+ ```bash
319
+ python -m {app_name} {required_arg}
320
+ ```
321
+ """
322
+ )
323
+ )
324
+
325
+ # Add more examples with optional parameters
326
+ for i, param in enumerate(optional_params[:3], 2):
327
+ if param.name in ["verbose", "quiet"]:
328
+ continue
329
+ example_value = param.choices[0] if param.choices else param.default
330
+ examples.append(
331
+ dedent(f"""
332
+ ### {i}. With {param.name} parameter
333
+
334
+ ```bash
335
+ python -m {app_name} --{param.name} {example_value} {required_arg}
336
+ ```
337
+ """)
338
+ )
339
+
340
+ markdown = dedent(
341
+ f"""
342
+ # Command Line Interface
343
+
344
+ Command line options for {app_name}
345
+
346
+ ```bash
347
+ python -m {app_name} [OPTIONS] {required_arg if required_params else ""}
348
+ ```
349
+
350
+ ## Options
351
+
352
+ {table}
353
+
354
+ ## Examples
355
+
356
+ {"".join(examples)}
357
+ """
358
+ ).strip()
359
+
360
+ Path(output_file).parent.mkdir(parents=True, exist_ok=True)
361
+ with open(output_file, "w", encoding="utf-8") as f:
362
+ f.write(markdown)