python-skills 1.0.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.
- python_skills/__init__.py +10 -0
- python_skills/__main__.py +6 -0
- python_skills/adapters/__init__.py +48 -0
- python_skills/adapters/agent_skills.py +415 -0
- python_skills/adapters/aider_adapter.py +226 -0
- python_skills/adapters/base.py +153 -0
- python_skills/adapters/claude.py +474 -0
- python_skills/adapters/cline.py +332 -0
- python_skills/adapters/codex.py +24 -0
- python_skills/adapters/continue_adapter.py +198 -0
- python_skills/adapters/cursor.py +327 -0
- python_skills/adapters/gemini.py +26 -0
- python_skills/adapters/goose.py +26 -0
- python_skills/adapters/junie.py +25 -0
- python_skills/adapters/kiro.py +382 -0
- python_skills/adapters/opencode.py +27 -0
- python_skills/adapters/roo.py +25 -0
- python_skills/adapters/universal.py +203 -0
- python_skills/adapters/vscode.py +27 -0
- python_skills/adapters/windsurf.py +26 -0
- python_skills/adapters/zed.py +27 -0
- python_skills/cli.py +326 -0
- python_skills/config.py +160 -0
- python_skills/detector.py +152 -0
- python_skills/installer.py +163 -0
- python_skills/markers.py +115 -0
- python_skills/skills/__init__.py +14 -0
- python_skills/skills/loader.py +171 -0
- python_skills/skills/metadata.py +152 -0
- python_skills/skills/registry.py +101 -0
- python_skills/state.py +204 -0
- python_skills-1.0.0.dist-info/METADATA +99 -0
- python_skills-1.0.0.dist-info/RECORD +105 -0
- python_skills-1.0.0.dist-info/WHEEL +4 -0
- python_skills-1.0.0.dist-info/entry_points.txt +2 -0
- python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
- skills/advanced_python.md +239 -0
- skills/anti_patterns/index.md +406 -0
- skills/comprehensions.md +167 -0
- skills/control_flow.md +175 -0
- skills/data_structures.md +243 -0
- skills/debugging/common_bugs.md +222 -0
- skills/debugging/inspection_techniques.md +249 -0
- skills/debugging/root_cause.md +203 -0
- skills/engineering/application_logging.md +195 -0
- skills/engineering/cli_apps.md +207 -0
- skills/engineering/configuration.md +218 -0
- skills/engineering/database.md +240 -0
- skills/engineering/dependency_management.md +205 -0
- skills/engineering/http_clients.md +267 -0
- skills/engineering/modules_packages.md +211 -0
- skills/engineering/packaging.md +197 -0
- skills/engineering/project_structure.md +155 -0
- skills/engineering/pyproject_toml.md +302 -0
- skills/engineering/virtual_environments.md +206 -0
- skills/functions.md +244 -0
- skills/generation/async_concurrency.md +291 -0
- skills/generation/error_handling.md +276 -0
- skills/generation/protocols_generics.md +243 -0
- skills/generation/type_hints.md +290 -0
- skills/generation/validation_pipeline.md +274 -0
- skills/generation/workflow.md +190 -0
- skills/oop.md +228 -0
- skills/quality/abstractions.md +154 -0
- skills/quality/comments.md +177 -0
- skills/quality/documentation.md +176 -0
- skills/quality/duplication.md +137 -0
- skills/quality/maintainability.md +142 -0
- skills/quality/naming.md +171 -0
- skills/quality/quality_functions.md +245 -0
- skills/quality/readability.md +239 -0
- skills/quality/type_annotations.md +192 -0
- skills/refactoring/behavior_preservation.md +157 -0
- skills/refactoring/incremental.md +187 -0
- skills/refactoring/interface_stability.md +199 -0
- skills/refactoring/safe_refactoring.md +206 -0
- skills/security/auth_boundaries.md +200 -0
- skills/security/command_injection.md +207 -0
- skills/security/dependency_risks.md +282 -0
- skills/security/file_handling.md +156 -0
- skills/security/input_validation.md +190 -0
- skills/security/path_traversal.md +172 -0
- skills/security/secrets.md +171 -0
- skills/security/sql_injection.md +188 -0
- skills/security/unsafe_deserialization.md +164 -0
- skills/stdlib/argparse.md +178 -0
- skills/stdlib/collections.md +212 -0
- skills/stdlib/datetime.md +187 -0
- skills/stdlib/functools.md +238 -0
- skills/stdlib/itertools.md +183 -0
- skills/stdlib/json.md +162 -0
- skills/stdlib/logging.md +185 -0
- skills/stdlib/os_sys.md +184 -0
- skills/stdlib/pathlib.md +218 -0
- skills/stdlib/re.md +171 -0
- skills/stdlib/statistics.md +112 -0
- skills/stdlib/subprocess.md +211 -0
- skills/testing/async_tests.md +249 -0
- skills/testing/coverage.md +168 -0
- skills/testing/edge_cases.md +197 -0
- skills/testing/fixtures_mocks.md +203 -0
- skills/testing/organization.md +205 -0
- skills/testing/parameterized.md +174 -0
- skills/testing/regression_tests.md +165 -0
- skills/variables_types.md +107 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Engineering: Logging (Application)
|
|
2
|
+
|
|
3
|
+
**Purpose**: Application-level logging setup and patterns.
|
|
4
|
+
|
|
5
|
+
**When to use**: Setting up logging for applications (not libraries).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Application vs Library Logging
|
|
12
|
+
- **Application**: Configures logging (handlers, formatters, levels)
|
|
13
|
+
- **Library**: Only uses `logging.getLogger(__name__)`, adds `NullHandler`
|
|
14
|
+
|
|
15
|
+
### Structured Logging (JSON)
|
|
16
|
+
```python
|
|
17
|
+
import logging
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
class JSONFormatter(logging.Formatter):
|
|
23
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
24
|
+
log_data = {
|
|
25
|
+
"timestamp": datetime.fromtimestamp(record.created).isoformat(),
|
|
26
|
+
"level": record.levelname,
|
|
27
|
+
"logger": record.name,
|
|
28
|
+
"message": record.getMessage(),
|
|
29
|
+
"module": record.module,
|
|
30
|
+
"function": record.funcName,
|
|
31
|
+
"line": record.lineno,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Add extra fields
|
|
35
|
+
for key, value in record.__dict__.items():
|
|
36
|
+
if key not in {"name", "msg", "args", "created", "filename", "funcName",
|
|
37
|
+
"levelname", "levelno", "lineno", "module", "msecs",
|
|
38
|
+
"message", "name", "pathname", "process", "processName",
|
|
39
|
+
"relativeCreated", "thread", "threadName", "exc_info",
|
|
40
|
+
"exc_text", "stack_info", "getMessage"}:
|
|
41
|
+
log_data[key] = value
|
|
42
|
+
|
|
43
|
+
if record.exc_info:
|
|
44
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
45
|
+
|
|
46
|
+
return json.dumps(log_data, default=str)
|
|
47
|
+
|
|
48
|
+
# Setup
|
|
49
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
50
|
+
handler.setFormatter(JSONFormatter())
|
|
51
|
+
root = logging.getLogger()
|
|
52
|
+
root.addHandler(handler)
|
|
53
|
+
root.setLevel(logging.INFO)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Structlog (Better Structured Logging)
|
|
57
|
+
```python
|
|
58
|
+
import structlog
|
|
59
|
+
|
|
60
|
+
structlog.configure(
|
|
61
|
+
processors=[
|
|
62
|
+
structlog.contextvars.merge_contextvars,
|
|
63
|
+
structlog.processors.add_log_level,
|
|
64
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
65
|
+
structlog.dev.ConsoleRenderer() if dev else structlog.processors.JSONRenderer(),
|
|
66
|
+
],
|
|
67
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
|
|
68
|
+
context_class=dict,
|
|
69
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
70
|
+
cache_logger_on_first_use=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
log = structlog.get_logger()
|
|
74
|
+
|
|
75
|
+
# Usage
|
|
76
|
+
log.info("user_login", user_id=123, ip="1.2.3.4")
|
|
77
|
+
log.error("db_failed", error=str(e), query="SELECT ...")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Context Injection
|
|
81
|
+
```python
|
|
82
|
+
import contextvars
|
|
83
|
+
|
|
84
|
+
request_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar("request_id", default=None)
|
|
85
|
+
|
|
86
|
+
class RequestIDFilter(logging.Filter):
|
|
87
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
88
|
+
record.request_id = request_id_var.get()
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# In middleware
|
|
92
|
+
request_id_var.set("req-123")
|
|
93
|
+
log.info("processing") # Includes request_id
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Log Levels Guide
|
|
97
|
+
| Level | Use Case |
|
|
98
|
+
|-------|----------|
|
|
99
|
+
| DEBUG | Detailed diagnostic (dev only) |
|
|
100
|
+
| INFO | General operations (requests, startup) |
|
|
101
|
+
| WARNING | Unexpected but handled (retry, fallback) |
|
|
102
|
+
| ERROR | Operation failed (5xx, failed request) |
|
|
103
|
+
| CRITICAL | System may stop (OOM, disk full) |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Decision Rules
|
|
108
|
+
|
|
109
|
+
| Situation | Setup |
|
|
110
|
+
|-----------|-------|
|
|
111
|
+
| Simple app | `logging.basicConfig` + JSON formatter |
|
|
112
|
+
| Production service | structlog + JSON + log aggregation |
|
|
113
|
+
| Library | `getLogger(__name__)` + `NullHandler` |
|
|
114
|
+
| CLI tool | Rich handler for pretty output |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Preferred Patterns
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Centralized setup (call once at startup)
|
|
122
|
+
def setup_logging(
|
|
123
|
+
level: str = "INFO",
|
|
124
|
+
json_format: bool = False,
|
|
125
|
+
dev_mode: bool = False,
|
|
126
|
+
) -> None:
|
|
127
|
+
import logging
|
|
128
|
+
import sys
|
|
129
|
+
|
|
130
|
+
# Clear existing
|
|
131
|
+
root = logging.getLogger()
|
|
132
|
+
for h in root.handlers[:]:
|
|
133
|
+
root.removeHandler(h)
|
|
134
|
+
|
|
135
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
136
|
+
|
|
137
|
+
if json_format and not dev_mode:
|
|
138
|
+
handler.setFormatter(JSONFormatter())
|
|
139
|
+
elif dev_mode:
|
|
140
|
+
import rich.logging
|
|
141
|
+
handler = rich.logging.RichHandler(rich_tracebacks=True)
|
|
142
|
+
else:
|
|
143
|
+
handler.setFormatter(logging.Formatter(
|
|
144
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
root.addHandler(handler)
|
|
148
|
+
root.setLevel(level)
|
|
149
|
+
|
|
150
|
+
# Quiet noisy libraries
|
|
151
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
152
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
153
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
154
|
+
|
|
155
|
+
# Usage in app
|
|
156
|
+
log = logging.getLogger(__name__)
|
|
157
|
+
|
|
158
|
+
def process_order(order_id: str):
|
|
159
|
+
log.info("order_started", order_id=order_id)
|
|
160
|
+
try:
|
|
161
|
+
do_work()
|
|
162
|
+
log.info("order_completed", order_id=order_id)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
log.exception("order_failed", order_id=order_id, error=str(e))
|
|
165
|
+
raise
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Avoid
|
|
171
|
+
|
|
172
|
+
- `print()` in production code
|
|
173
|
+
- Logging sensitive data (passwords, tokens, PII)
|
|
174
|
+
- Excessive DEBUG in production
|
|
175
|
+
- No log rotation (use `RotatingFileHandler` or external)
|
|
176
|
+
- Blocking I/O in logging (use `QueueHandler` + `QueueListener` for high volume)
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Validation Considerations
|
|
181
|
+
|
|
182
|
+
- Test log output format
|
|
183
|
+
- Verify log levels in different environments
|
|
184
|
+
- Check structured fields are present
|
|
185
|
+
- Ensure no PII in logs
|
|
186
|
+
- Test high-volume logging doesn't block
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Related Skills
|
|
191
|
+
|
|
192
|
+
- `stdlib/logging.md`
|
|
193
|
+
- `engineering/configuration.md`
|
|
194
|
+
- `security/secrets.md`
|
|
195
|
+
- `generation/error_handling.md`
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Engineering: CLI Applications
|
|
2
|
+
|
|
3
|
+
**Purpose**: Building command-line interfaces with Python.
|
|
4
|
+
|
|
5
|
+
**When to use**: Creating CLI tools, scripts, or application entry points.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Entry Points
|
|
12
|
+
```toml
|
|
13
|
+
# pyproject.toml
|
|
14
|
+
[project.scripts]
|
|
15
|
+
my-cli = "mypackage.cli:main"
|
|
16
|
+
my-other = "mypackage.other:run"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Click (Recommended for Complex CLIs)
|
|
20
|
+
```python
|
|
21
|
+
import click
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
@click.version_option()
|
|
26
|
+
@click.option("-v", "--verbose", count=True)
|
|
27
|
+
@click.pass_context
|
|
28
|
+
def cli(ctx: click.Context, verbose: int):
|
|
29
|
+
"""My CLI tool."""
|
|
30
|
+
ctx.ensure_object(dict)
|
|
31
|
+
ctx.obj["verbose"] = verbose
|
|
32
|
+
|
|
33
|
+
@cli.command()
|
|
34
|
+
@click.argument("input", type=click.Path(exists=True, path_type=Path))
|
|
35
|
+
@click.option("-o", "--output", type=click.Path(path_type=Path))
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def process(ctx: click.Context, input: Path, output: Path | None):
|
|
38
|
+
"""Process a file."""
|
|
39
|
+
verbose = ctx.obj["verbose"]
|
|
40
|
+
if verbose:
|
|
41
|
+
click.echo(f"Processing {input}")
|
|
42
|
+
# ...
|
|
43
|
+
|
|
44
|
+
@cli.command()
|
|
45
|
+
@click.option("--force", is_flag=True)
|
|
46
|
+
def clean(force: bool):
|
|
47
|
+
"""Clean cache."""
|
|
48
|
+
if not force:
|
|
49
|
+
click.confirm("Delete all cache?", abort=True)
|
|
50
|
+
# ...
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
cli()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Typer (Modern, Type-Hint Based)
|
|
57
|
+
```python
|
|
58
|
+
import typer
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from typing_extensions import Annotated
|
|
61
|
+
|
|
62
|
+
app = typer.Typer()
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def process(
|
|
66
|
+
input: Annotated[Path, typer.Argument(exists=True, help="Input file")],
|
|
67
|
+
output: Annotated[Path | None, typer.Option("-o", "--output")] = None,
|
|
68
|
+
verbose: Annotated[int, typer.Option("-v", "--verbose", count=True)] = 0,
|
|
69
|
+
):
|
|
70
|
+
"""Process a file."""
|
|
71
|
+
if verbose:
|
|
72
|
+
typer.echo(f"Processing {input}")
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def clean(force: Annotated[bool, typer.Option("--force", help="Skip confirmation")] = False):
|
|
76
|
+
"""Clean cache."""
|
|
77
|
+
if not force:
|
|
78
|
+
typer.confirm("Delete all cache?", abort=True)
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
app()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### argparse (Stdlib Only)
|
|
85
|
+
```python
|
|
86
|
+
import argparse
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
90
|
+
parser = argparse.ArgumentParser(
|
|
91
|
+
prog="my-cli",
|
|
92
|
+
description="Description",
|
|
93
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument("input", type=Path)
|
|
96
|
+
parser.add_argument("-o", "--output", type=Path)
|
|
97
|
+
parser.add_argument("-v", "--verbose", action="count", default=0)
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
def main(argv: list[str] | None = None) -> int:
|
|
101
|
+
parser = create_parser()
|
|
102
|
+
args = parser.parse_args(argv)
|
|
103
|
+
# ...
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
sys.exit(main())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Rich (Beautiful Output)
|
|
111
|
+
```python
|
|
112
|
+
from rich.console import Console
|
|
113
|
+
from rich.table import Table
|
|
114
|
+
from rich.progress import Progress
|
|
115
|
+
|
|
116
|
+
console = Console()
|
|
117
|
+
|
|
118
|
+
console.print("[bold green]Success![/bold green]")
|
|
119
|
+
console.print("[red]Error:[/red] Something failed")
|
|
120
|
+
|
|
121
|
+
table = Table(title="Results")
|
|
122
|
+
table.add_column("Name")
|
|
123
|
+
table.add_column("Status")
|
|
124
|
+
table.add_row("Item 1", "[green]OK[/green]")
|
|
125
|
+
table.add_row("Item 2", "[red]FAIL[/red]")
|
|
126
|
+
console.print(table)
|
|
127
|
+
|
|
128
|
+
with Progress() as progress:
|
|
129
|
+
task = progress.add_task("Processing...", total=100)
|
|
130
|
+
for i in range(100):
|
|
131
|
+
progress.update(task, advance=1)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Decision Rules
|
|
137
|
+
|
|
138
|
+
| Complexity | Tool |
|
|
139
|
+
|------------|------|
|
|
140
|
+
| Simple (1-3 commands) | `argparse` (stdlib) |
|
|
141
|
+
| Multiple commands, options | `click` |
|
|
142
|
+
| Modern, type-hint focused | `typer` |
|
|
143
|
+
| Beautiful output needed | `rich` (with any) |
|
|
144
|
+
| No dependencies allowed | `argparse` |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Preferred Patterns
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Common structure
|
|
152
|
+
# src/mypackage/cli.py
|
|
153
|
+
|
|
154
|
+
def main(argv: list[str] | None = None) -> int:
|
|
155
|
+
"""Entry point for setuptools console_scripts."""
|
|
156
|
+
try:
|
|
157
|
+
# Parse args, run logic
|
|
158
|
+
return 0
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
return 130
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.exception("Fatal error")
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
sys.exit(main())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Subcommand Organization
|
|
170
|
+
```
|
|
171
|
+
cli.py # Main entry, creates command group
|
|
172
|
+
commands/
|
|
173
|
+
__init__.py # Imports and registers subcommands
|
|
174
|
+
process.py # @cli.command() or @app.command()
|
|
175
|
+
clean.py
|
|
176
|
+
config.py
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Avoid
|
|
182
|
+
|
|
183
|
+
- Logic in `__main__.py` (use entry point function)
|
|
184
|
+
- Global state in CLI module
|
|
185
|
+
- Printing directly (use `click.echo`, `typer.echo`, `console.print`)
|
|
186
|
+
- No `--help` / `--version`
|
|
187
|
+
- Exit codes not meaningful (0=success, 1=error, 130=interrupt)
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Validation Considerations
|
|
192
|
+
|
|
193
|
+
- `my-cli --help` works
|
|
194
|
+
- `my-cli --version` shows version
|
|
195
|
+
- Invalid args show usage + exit 2
|
|
196
|
+
- Tests for each command
|
|
197
|
+
- Shell completion (`click`/`typer` support)
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Related Skills
|
|
202
|
+
|
|
203
|
+
- `stdlib/argparse.md`
|
|
204
|
+
- `engineering/configuration.md`
|
|
205
|
+
- `engineering/packaging.md`
|
|
206
|
+
- `generation/error_handling.md`
|
|
207
|
+
- `testing/organization.md`
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Engineering: Configuration
|
|
2
|
+
|
|
3
|
+
**Purpose**: Application configuration management patterns.
|
|
4
|
+
|
|
5
|
+
**When to use**: Any application needing configuration (CLI, server, library).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Configuration Sources (Priority Order)
|
|
12
|
+
1. **Defaults** — Code defaults (lowest)
|
|
13
|
+
2. **Config file** — TOML/YAML/JSON file
|
|
14
|
+
3. **Environment variables** — Override config file
|
|
15
|
+
4. **CLI arguments** — Highest priority (for apps)
|
|
16
|
+
|
|
17
|
+
### Layered Configuration
|
|
18
|
+
```python
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Optional
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Config:
|
|
26
|
+
# Required (no default)
|
|
27
|
+
database_url: str
|
|
28
|
+
|
|
29
|
+
# Optional with defaults
|
|
30
|
+
host: str = "0.0.0.0"
|
|
31
|
+
port: int = 8000
|
|
32
|
+
debug: bool = False
|
|
33
|
+
log_level: str = "INFO"
|
|
34
|
+
|
|
35
|
+
# Nested config
|
|
36
|
+
redis: "RedisConfig" = field(default_factory=RedisConfig)
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class RedisConfig:
|
|
40
|
+
host: str = "localhost"
|
|
41
|
+
port: int = 6379
|
|
42
|
+
db: int = 0
|
|
43
|
+
password: Optional[str] = None
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Environment Variable Mapping
|
|
47
|
+
```python
|
|
48
|
+
import os
|
|
49
|
+
from dataclasses import fields
|
|
50
|
+
|
|
51
|
+
def load_from_env(config: Config, prefix: str = "APP_") -> Config:
|
|
52
|
+
"""Update config from environment variables."""
|
|
53
|
+
for f in fields(config):
|
|
54
|
+
env_key = f"{prefix}{f.name.upper()}"
|
|
55
|
+
if env_key in os.environ:
|
|
56
|
+
value = os.environ[env_key]
|
|
57
|
+
# Type conversion
|
|
58
|
+
if f.type == bool:
|
|
59
|
+
value = value.lower() in ("1", "true", "yes", "on")
|
|
60
|
+
elif f.type == int:
|
|
61
|
+
value = int(value)
|
|
62
|
+
elif f.type == list[str]:
|
|
63
|
+
value = value.split(",")
|
|
64
|
+
setattr(config, f.name, value)
|
|
65
|
+
return config
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Config File Loading (TOML)
|
|
69
|
+
```python
|
|
70
|
+
import tomllib # Python 3.11+
|
|
71
|
+
|
|
72
|
+
def load_config_file(path: Path) -> dict:
|
|
73
|
+
with path.open("rb") as f:
|
|
74
|
+
return tomllib.load(f)
|
|
75
|
+
|
|
76
|
+
# For older Python: pip install tomli
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Complete Loader
|
|
80
|
+
```python
|
|
81
|
+
def load_config(
|
|
82
|
+
config_path: Path | None = None,
|
|
83
|
+
env_prefix: str = "APP_",
|
|
84
|
+
cli_overrides: dict | None = None,
|
|
85
|
+
) -> Config:
|
|
86
|
+
# 1. Defaults
|
|
87
|
+
config = Config(database_url="") # Will be validated
|
|
88
|
+
|
|
89
|
+
# 2. Config file
|
|
90
|
+
if config_path and config_path.exists():
|
|
91
|
+
file_config = load_config_file(config_path)
|
|
92
|
+
apply_dict(config, file_config)
|
|
93
|
+
|
|
94
|
+
# 3. Environment
|
|
95
|
+
load_from_env(config, env_prefix)
|
|
96
|
+
|
|
97
|
+
# 4. CLI overrides
|
|
98
|
+
if cli_overrides:
|
|
99
|
+
apply_dict(config, cli_overrides)
|
|
100
|
+
|
|
101
|
+
# 5. Validate
|
|
102
|
+
validate_config(config)
|
|
103
|
+
|
|
104
|
+
return config
|
|
105
|
+
|
|
106
|
+
def apply_dict(config: Config, data: dict) -> None:
|
|
107
|
+
for key, value in data.items():
|
|
108
|
+
if hasattr(config, key):
|
|
109
|
+
setattr(config, key, value)
|
|
110
|
+
elif hasattr(config, key.replace("-", "_")):
|
|
111
|
+
setattr(config, key.replace("-", "_"), value)
|
|
112
|
+
|
|
113
|
+
def validate_config(config: Config) -> None:
|
|
114
|
+
if not config.database_url:
|
|
115
|
+
raise ValueError("database_url is required")
|
|
116
|
+
if not 1 <= config.port <= 65535:
|
|
117
|
+
raise ValueError("Invalid port")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Pydantic Settings (Alternative)
|
|
121
|
+
```python
|
|
122
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
123
|
+
|
|
124
|
+
class Settings(BaseSettings):
|
|
125
|
+
database_url: str
|
|
126
|
+
host: str = "0.0.0.0"
|
|
127
|
+
port: int = 8000
|
|
128
|
+
debug: bool = False
|
|
129
|
+
|
|
130
|
+
model_config = SettingsConfigDict(
|
|
131
|
+
env_file=".env",
|
|
132
|
+
env_file_encoding="utf-8",
|
|
133
|
+
env_prefix="APP_",
|
|
134
|
+
case_sensitive=False,
|
|
135
|
+
extra="ignore",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
settings = Settings() # Auto-loads from env + .env
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Decision Rules
|
|
144
|
+
|
|
145
|
+
| Need | Approach |
|
|
146
|
+
|------|----------|
|
|
147
|
+
| Simple app | Dataclass + env vars |
|
|
148
|
+
| Complex validation | Pydantic Settings |
|
|
149
|
+
| Multiple environments | Config files per env + env vars |
|
|
150
|
+
| Secrets | Environment variables only (never files) |
|
|
151
|
+
| Feature flags | Config + env override |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Preferred Patterns
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Central config instance
|
|
159
|
+
_config: Config | None = None
|
|
160
|
+
|
|
161
|
+
def get_config() -> Config:
|
|
162
|
+
global _config
|
|
163
|
+
if _config is None:
|
|
164
|
+
_config = load_config()
|
|
165
|
+
return _config
|
|
166
|
+
|
|
167
|
+
# Reset for testing
|
|
168
|
+
def reset_config() -> None:
|
|
169
|
+
global _config
|
|
170
|
+
_config = None
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Secrets Management
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# NEVER hardcode secrets
|
|
179
|
+
# NEVER commit .env files with real secrets
|
|
180
|
+
# Use: Environment variables, secret managers (AWS Secrets, Vault, etc.)
|
|
181
|
+
|
|
182
|
+
# .env.example (committed)
|
|
183
|
+
DATABASE_URL=postgresql://user:pass@localhost/db
|
|
184
|
+
API_KEY=
|
|
185
|
+
|
|
186
|
+
# .env (gitignored, local only)
|
|
187
|
+
DATABASE_URL=postgresql://prod:secret@db/prod
|
|
188
|
+
API_KEY=sk-live-...
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Avoid
|
|
194
|
+
|
|
195
|
+
- Global mutable config (use frozen dataclass or singleton getter)
|
|
196
|
+
- Config file with secrets
|
|
197
|
+
- Complex inheritance in config
|
|
198
|
+
- Silent failures on missing required config
|
|
199
|
+
- Type confusion (env vars are strings)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Validation Considerations
|
|
204
|
+
|
|
205
|
+
- Test config loading with various sources
|
|
206
|
+
- Validate required fields
|
|
207
|
+
- Test type conversion edge cases
|
|
208
|
+
- Ensure secrets never logged
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Related Skills
|
|
213
|
+
|
|
214
|
+
- `engineering/pyproject_toml.md`
|
|
215
|
+
- `engineering/cli_apps.md`
|
|
216
|
+
- `security/secrets.md`
|
|
217
|
+
- `generation/type_hints.md`
|
|
218
|
+
- `generation/error_handling.md`
|