python-template-project 0.1.0__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.
@@ -0,0 +1,557 @@
1
+ """Central configuration management for python_template_project project.
2
+
3
+ This module provides a single source of truth for all configuration parameters
4
+ organized in categories (CLI, App, GUI). It can generate config files, CLI modules,
5
+ and documentation from the parameter definitions.
6
+ """
7
+
8
+ import json
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from textwrap import dedent
12
+ from typing import Any
13
+
14
+ import yaml # type: ignore
15
+ from pydantic import BaseModel # type: ignore
16
+
17
+
18
+ @dataclass
19
+ class ConfigParameter:
20
+ """Represents a single configuration parameter with all its metadata."""
21
+
22
+ name: str
23
+ default: Any
24
+ type_: type
25
+ choices: list[str | bool | int] | None = None
26
+ help: str = ""
27
+ cli_arg: str | None = None
28
+ required: bool = False
29
+
30
+ def __post_init__(self):
31
+ if self.cli_arg is None and not self.required:
32
+ self.cli_arg = f"--{self.name}"
33
+ if self.type_ is bool:
34
+ self.choices = [True, False]
35
+
36
+
37
+ class CliConfig(BaseModel):
38
+ """CLI-specific configuration parameters."""
39
+
40
+ # Positional argument
41
+ input: ConfigParameter = ConfigParameter(
42
+ name="input",
43
+ default="",
44
+ type_=str,
45
+ help="Path to input (file or folder)",
46
+ required=True,
47
+ cli_arg=None, # Positional argument
48
+ )
49
+
50
+ # Optional CLI arguments
51
+ output: ConfigParameter = ConfigParameter(
52
+ name="output",
53
+ default="",
54
+ type_=str,
55
+ help="Path to output destination",
56
+ )
57
+
58
+ min_dist: ConfigParameter = ConfigParameter(
59
+ name="min_dist",
60
+ default=20,
61
+ type_=int,
62
+ help="Maximum distance between two waypoints",
63
+ )
64
+
65
+ extract_waypoints: ConfigParameter = ConfigParameter(
66
+ name="extract_waypoints",
67
+ default=True,
68
+ type_=bool,
69
+ help="Extract starting points of each track as waypoint",
70
+ )
71
+
72
+ elevation: ConfigParameter = ConfigParameter(
73
+ name="elevation",
74
+ default=True,
75
+ type_=bool,
76
+ help="Include elevation data in waypoints",
77
+ )
78
+
79
+
80
+ class AppConfig(BaseModel):
81
+ """Application-specific configuration parameters."""
82
+
83
+ date_format: ConfigParameter = ConfigParameter(
84
+ name="date_format",
85
+ default="%Y-%m-%d",
86
+ type_=str,
87
+ help="Date format to use",
88
+ )
89
+
90
+ log_level: ConfigParameter = ConfigParameter(
91
+ name="log_level",
92
+ default="INFO",
93
+ type_=str,
94
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
95
+ help="Logging level for the application",
96
+ )
97
+
98
+ log_file_max_size: ConfigParameter = ConfigParameter(
99
+ name="log_file_max_size",
100
+ default=10,
101
+ type_=int,
102
+ help="Maximum log file size in MB before rotation",
103
+ )
104
+
105
+ log_backup_count: ConfigParameter = ConfigParameter(
106
+ name="log_backup_count",
107
+ default=5,
108
+ type_=int,
109
+ help="Number of backup log files to keep",
110
+ )
111
+
112
+ log_format: ConfigParameter = ConfigParameter(
113
+ name="log_format",
114
+ default="detailed",
115
+ type_=str,
116
+ choices=["simple", "detailed", "json"],
117
+ help="Log message format style",
118
+ )
119
+
120
+ max_workers: ConfigParameter = ConfigParameter(
121
+ name="max_workers",
122
+ default=4,
123
+ type_=int,
124
+ help="Maximum number of worker threads",
125
+ )
126
+
127
+ enable_file_logging: ConfigParameter = ConfigParameter(
128
+ name="enable_file_logging",
129
+ default=True,
130
+ type_=bool,
131
+ help="Enable logging to file",
132
+ )
133
+
134
+ enable_console_logging: ConfigParameter = ConfigParameter(
135
+ name="enable_console_logging",
136
+ default=True,
137
+ type_=bool,
138
+ help="Enable logging to console",
139
+ )
140
+
141
+
142
+ class GuiConfig(BaseModel):
143
+ """GUI-specific configuration parameters."""
144
+
145
+ theme: ConfigParameter = ConfigParameter(
146
+ name="theme",
147
+ default="light",
148
+ type_=str,
149
+ choices=["light", "dark", "auto"],
150
+ help="GUI theme setting",
151
+ )
152
+
153
+ window_width: ConfigParameter = ConfigParameter(
154
+ name="window_width",
155
+ default=800,
156
+ type_=int,
157
+ help="Default window width",
158
+ )
159
+
160
+ window_height: ConfigParameter = ConfigParameter(
161
+ name="window_height",
162
+ default=600,
163
+ type_=int,
164
+ help="Default window height",
165
+ )
166
+
167
+ log_window_height: ConfigParameter = ConfigParameter(
168
+ name="log_window_height",
169
+ default=200,
170
+ type_=int,
171
+ help="Height of the log window in pixels",
172
+ )
173
+
174
+ auto_scroll_log: ConfigParameter = ConfigParameter(
175
+ name="auto_scroll_log",
176
+ default=True,
177
+ type_=bool,
178
+ help="Automatically scroll to newest log entries",
179
+ )
180
+
181
+ max_log_lines: ConfigParameter = ConfigParameter(
182
+ name="max_log_lines",
183
+ default=1000,
184
+ type_=int,
185
+ help="Maximum number of log lines to keep in GUI",
186
+ )
187
+
188
+
189
+ class ConfigParameterManager(BaseModel):
190
+ """Main configuration manager that handles all parameter categories."""
191
+
192
+ cli: CliConfig = CliConfig()
193
+ app: AppConfig = AppConfig()
194
+ gui: GuiConfig = GuiConfig()
195
+
196
+ def __init__(self, config_file: str | None = None, **kwargs):
197
+ """Initialize configuration from file and/or keyword arguments.
198
+
199
+ Args:
200
+ config_file: Path to configuration file (JSON or YAML)
201
+ **kwargs: Override parameters in format category__parameter (e.g., cli__output)
202
+ """
203
+ super().__init__()
204
+
205
+ # Load from file if provided
206
+ if config_file:
207
+ self.load_from_file(config_file)
208
+
209
+ # Override with provided kwargs
210
+ self._apply_kwargs(kwargs)
211
+
212
+ def _apply_kwargs(self, kwargs: dict[str, Any]):
213
+ """Apply keyword arguments to override configuration values.
214
+
215
+ Args:
216
+ kwargs: Dictionary with keys in format 'category__parameter'
217
+ """
218
+ for key, value in kwargs.items():
219
+ if "__" in key:
220
+ category, param_name = key.split("__", 1)
221
+ if hasattr(self, category):
222
+ category_obj = getattr(self, category)
223
+ if hasattr(category_obj, param_name):
224
+ param = getattr(category_obj, param_name)
225
+ param.default = value
226
+
227
+ def load_from_file(self, config_file: str):
228
+ """Load configuration from JSON or YAML file."""
229
+ config_path = Path(config_file)
230
+ if not config_path.exists():
231
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
232
+
233
+ with open(config_path, "r", encoding="utf-8") as f:
234
+ if config_path.suffix.lower() in [".yml", ".yaml"]:
235
+ config_data = yaml.safe_load(f)
236
+ else:
237
+ config_data = json.load(f)
238
+
239
+ # Apply loaded configuration
240
+ for category_name, category_data in config_data.items():
241
+ if hasattr(self, category_name):
242
+ category_obj = getattr(self, category_name)
243
+ for param_name, param_value in category_data.items():
244
+ if hasattr(category_obj, param_name):
245
+ param = getattr(category_obj, param_name)
246
+ param.default = param_value
247
+
248
+ def save_to_file(self, config_file: str, format_: str = "auto"):
249
+ """Save current configuration to file.
250
+
251
+ Args:
252
+ config_file: Path to save configuration
253
+ format_: Format to use ('json', 'yaml', or 'auto' to detect from extension)
254
+ """
255
+ config_path = Path(config_file)
256
+ config_data = self.to_dict()
257
+
258
+ # Determine format
259
+ if format_ == "auto":
260
+ format_ = "yaml" if config_path.suffix.lower() in [".yml", ".yaml"] else "json"
261
+
262
+ # Ensure directory exists
263
+ config_path.parent.mkdir(parents=True, exist_ok=True)
264
+
265
+ with open(config_path, "w", encoding="utf-8") as f:
266
+ if format_ == "yaml":
267
+ yaml.dump(config_data, f, default_flow_style=False, indent=2)
268
+ else:
269
+ json.dump(config_data, f, indent=2)
270
+
271
+ def to_dict(self) -> dict[str, Any]:
272
+ """Convert configuration to dictionary."""
273
+ result = {}
274
+ for category_name in self.model_fields.keys():
275
+ category_obj = getattr(self, category_name)
276
+ category_dict = {}
277
+ for field_name in category_obj.model_fields:
278
+ param = getattr(category_obj, field_name)
279
+ category_dict[param.name] = param.default
280
+ result[category_name] = category_dict
281
+ return result
282
+
283
+ def get_all_parameters(self) -> list[ConfigParameter]:
284
+ """Get all parameters from all categories."""
285
+ parameters = []
286
+ for category_name in self.model_fields.keys():
287
+ category_obj = getattr(self, category_name)
288
+ for field_name in category_obj.model_fields:
289
+ param = getattr(category_obj, field_name)
290
+ parameters.append(param)
291
+ return parameters
292
+
293
+ def get_cli_parameters(self) -> list[ConfigParameter]:
294
+ """Get only CLI parameters."""
295
+ cli_params = []
296
+ for field_name in type(self.cli).model_fields.keys():
297
+ param = getattr(self.cli, field_name)
298
+ cli_params.append(param)
299
+ return cli_params
300
+
301
+ def get_logging_config(self) -> dict[str, Any]:
302
+ """Get logging-specific configuration as dictionary.
303
+
304
+ Returns:
305
+ Dictionary with logging configuration parameters
306
+ """
307
+ return {
308
+ "log_level": self.app.log_level.default,
309
+ "log_file_max_size": self.app.log_file_max_size.default
310
+ * 1024
311
+ * 1024, # Convert MB to bytes
312
+ "log_backup_count": self.app.log_backup_count.default,
313
+ "log_format": self.app.log_format.default,
314
+ "enable_file_logging": self.app.enable_file_logging.default,
315
+ "enable_console_logging": self.app.enable_console_logging.default,
316
+ "max_log_lines": self.gui.max_log_lines.default,
317
+ "auto_scroll_log": self.gui.auto_scroll_log.default,
318
+ }
319
+
320
+ @classmethod
321
+ def generate_config_markdown_doc(cls, output_file: str):
322
+ """Generate a Markdown documentation for all
323
+ configuration parameters organized by category."""
324
+
325
+ manager = cls()
326
+
327
+ def pad(s, width):
328
+ return s + " " * (width - len(s))
329
+
330
+ markdown_content = dedent(
331
+ """
332
+ # Config parameters
333
+
334
+ These parameters are available to configure the behavior of your application.
335
+ The parameters in the cli category can be accessed via the command line interface.
336
+
337
+ """
338
+ ).lstrip()
339
+
340
+ for category_name in cls.model_fields.keys():
341
+ category_obj = getattr(manager, category_name)
342
+ markdown_content += f'## Category "{category_name}"\n\n'
343
+
344
+ # Collect all parameters for this category
345
+ rows = []
346
+ header = ["Name", "Type", "Description", "Default", "Choices"]
347
+
348
+ for field_name in type(category_obj).model_fields.keys():
349
+ param = getattr(category_obj, field_name)
350
+ name = param.name
351
+ typ = param.type_.__name__
352
+ desc = param.help
353
+ default = repr(param.default)
354
+ choices = str(param.choices) if param.choices else "-"
355
+
356
+ rows.append((name, typ, desc, default, choices))
357
+
358
+ # Calculate column widths
359
+ all_rows = [header] + rows
360
+ widths = [max(len(str(col)) for col in column) for column in zip(*all_rows)]
361
+
362
+ # Create Markdown table
363
+ table = (
364
+ "| "
365
+ + " | ".join(pad(h, w) for h, w in zip(header, widths))
366
+ + " |\n"
367
+ + "|-"
368
+ + "-|-".join("-" * w for w in widths)
369
+ + "-|\n"
370
+ )
371
+ for row in rows:
372
+ table += "| " + " | ".join(pad(str(col), w) for col, w in zip(row, widths)) + " |\n"
373
+
374
+ markdown_content += table + "\n"
375
+
376
+ # Write to file
377
+ Path(output_file).parent.mkdir(parents=True, exist_ok=True)
378
+ with open(output_file, "w", encoding="utf-8") as f:
379
+ f.write(markdown_content)
380
+
381
+ @classmethod
382
+ def generate_default_config_file(cls, output_file: str):
383
+ """Generate a default configuration file with all parameters and their descriptions."""
384
+ manager = cls()
385
+ output_path = Path(output_file)
386
+ output_path.parent.mkdir(parents=True, exist_ok=True)
387
+
388
+ with open(output_path, "w", encoding="utf-8") as f:
389
+ f.write("# Configuration File\n")
390
+ f.write("# This file was auto-generated. Modify as needed.\n\n")
391
+
392
+ for category_name in cls.model_fields.keys():
393
+ category_obj = getattr(manager, category_name)
394
+ f.write(f"# {category_name.upper()} Configuration\n")
395
+ f.write(f"{category_name}:\n")
396
+
397
+ for field_name in type(category_obj).model_fields.keys():
398
+ param = getattr(category_obj, field_name)
399
+ f.write(f" # {param.help}\n")
400
+ if param.choices:
401
+ f.write(f" # Choices: {param.choices}\n")
402
+ f.write(f" # Type: {param.type_.__name__}\n")
403
+ f.write(f" {param.name}: {repr(param.default)}\n\n")
404
+
405
+ f.write("\n")
406
+
407
+ @classmethod
408
+ def generate_cli_markdown_doc(cls, output_file: str):
409
+ """Generate a Markdown CLI documentation with a formatted table and examples."""
410
+ manager = cls()
411
+ cli_params = manager.get_cli_parameters()
412
+
413
+ rows = []
414
+ required_params = []
415
+ optional_params = []
416
+
417
+ for param in cli_params:
418
+ cli_arg = f"`--{param.name}`" if param.name != "input" else "`path/to/file`"
419
+ typ = param.type_.__name__
420
+ desc = param.help
421
+ default = (
422
+ "*required*"
423
+ if param.required or param.default in (None, "")
424
+ else repr(param.default)
425
+ )
426
+ choices = str(param.choices) if param.choices else "-"
427
+
428
+ rows.append((cli_arg, typ, desc, default, choices))
429
+ if default == "*required*":
430
+ required_params.append(param)
431
+ else:
432
+ optional_params.append(param)
433
+
434
+ # Dynamically determine column width
435
+ def pad(s, width):
436
+ return s + " " * (width - len(s))
437
+
438
+ widths = [max(len(str(col)) for col in column) for column in zip(*rows)]
439
+ header = ["Option", "Type", "Description", "Default", "Choices"]
440
+
441
+ # Create Markdown table
442
+ table = (
443
+ "| "
444
+ + " | ".join(pad(h, w) for h, w in zip(header, widths))
445
+ + " |\n"
446
+ + "|-"
447
+ + "-|-".join("-" * w for w in widths)
448
+ + "-|\n"
449
+ )
450
+ for row in rows:
451
+ table += "| " + " | ".join(pad(str(col), w) for col, w in zip(row, widths)) + " |\n"
452
+
453
+ # Generate example commands
454
+ examples = []
455
+ required_arg = required_params[0].name if required_params else "example.input"
456
+ examples.append(
457
+ dedent(
458
+ f"""
459
+ ### 1. Standard version (only required parameter)
460
+
461
+ ```bash
462
+ python -m python_template_project.cli {required_arg}
463
+ ```
464
+ """
465
+ )
466
+ )
467
+
468
+ # Add logging examples
469
+ examples.append(
470
+ dedent(
471
+ f"""
472
+ ### 2. With verbose logging
473
+
474
+ ```bash
475
+ python -m python_template_project.cli --verbose {required_arg}
476
+ ```
477
+ """
478
+ )
479
+ )
480
+
481
+ examples.append(
482
+ dedent(
483
+ f"""
484
+ ### 3. With quiet mode
485
+
486
+ ```bash
487
+ python -m python_template_project.cli --quiet {required_arg}
488
+ ```
489
+ """
490
+ )
491
+ )
492
+
493
+ for i in range(1, min(3, len(optional_params) + 1)):
494
+ selected = [p for p in optional_params if p.name not in ["verbose", "quiet"]][:i]
495
+ if selected:
496
+ cli_part = " ".join(
497
+ f"--{p.name} {p.choices[0] if p.choices else p.default}" for p in selected
498
+ )
499
+ examples.append(
500
+ dedent(
501
+ f"""
502
+ ### {i + 3}. Example with {i} Parameter(s)
503
+
504
+ ```bash
505
+ python -m python_template_project.cli {cli_part} {required_arg}
506
+ ```
507
+ """
508
+ )
509
+ )
510
+
511
+ markdown = dedent(
512
+ """
513
+ # Command line interface
514
+
515
+ Command line options
516
+
517
+ ```bash
518
+ python -m python_template_project.cli [OPTIONS] path/to/file
519
+ ```
520
+
521
+ ---
522
+
523
+ ## ⚙️ CLI-Options
524
+
525
+ {}
526
+
527
+ ## 💡 Examples
528
+
529
+ In the example, the following is assumed: `example.input` in the current directory
530
+
531
+ {}
532
+ """
533
+ ).format(table, "".join(examples))
534
+
535
+ Path(output_file).parent.mkdir(parents=True, exist_ok=True)
536
+ with open(output_file, "w", encoding="utf-8") as f:
537
+ f.write(markdown.strip())
538
+
539
+
540
+ def main():
541
+ """Main function to generate config file and documentation."""
542
+ default_config: str = "../../config.yaml"
543
+ default_cli_doc: str = "../../docs/usage/cli.md"
544
+ default_config_doc: str = "../../docs/usage/config.md"
545
+
546
+ ConfigParameterManager.generate_default_config_file(default_config)
547
+ print(f"Generated: {default_config}")
548
+
549
+ ConfigParameterManager.generate_config_markdown_doc(default_config_doc)
550
+ print(f"Generated: {default_config_doc}")
551
+
552
+ ConfigParameterManager.generate_cli_markdown_doc(default_cli_doc)
553
+ print(f"Generated: {default_cli_doc}")
554
+
555
+
556
+ if __name__ == "__main__":
557
+ main()
File without changes