Skip to content

Commit 15bfaff

Browse files
committed
refactor(config): migrate configuration to new structure
1 parent f2b7d73 commit 15bfaff

File tree

14 files changed

+53
-101
lines changed

14 files changed

+53
-101
lines changed

docs/contributing/adding-service.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ If the service only supports a subset of regions, rely on `session.get_available
5353

5454
## 4. Update defaults and documentation
5555

56-
- Append the service name to `aws.services` in `src/costcutter/conf/config.yaml`
56+
- Append the service name to `aws.services` defaults in `src/costcutter/config.py`
5757
- Refresh relevant documentation (for example `docs/guide/supported-services.md`)
5858

5959
## 5. Write tests

docs/guide/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This section explains how the main pieces of CostCutter fit together and why the
99
- Renders a Rich live table of events while orchestration runs and a summary when it finishes
1010
- Handles banner rendering, CSV export messaging, and keyboard interrupt handling
1111

12-
**Configuration loader (`src/costcutter/conf/config.py`)**
12+
**Configuration loader (`src/costcutter/config.py`)**
1313
- Loads the default YAML file bundled with the package
1414
- Merges home directory overrides, an explicit file, environment variables, and CLI arguments
1515
- Exposes the merged result through a lightweight `Config` wrapper that supports attribute access
@@ -38,7 +38,7 @@ This section explains how the main pieces of CostCutter fit together and why the
3838
## Data flow
3939

4040
1. CLI parses arguments and clears the terminal
41-
2. `get_config` merges configuration sources into a single object
41+
2. `load_config` merges configuration sources into a single object
4242
3. Logging is initialised based on the merged configuration
4343
4. The orchestrator resolves regions and services, then creates an AWS session via `create_aws_session`
4444
5. Each `(region, service)` pair runs in the thread pool and calls the appropriate handler

docs/guide/config-reference.md

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Complete guide to configuring CostCutter.
66

77
CostCutter merges configuration from multiple sources in this order (later sources override earlier ones):
88

9-
1. **Default Config** - Built-in defaults (`src/costcutter/conf/config.yaml`)
9+
1. **Default Config** - Built-in defaults (defined in `src/costcutter/config.py`)
1010
2. **Home Config** - User's home directory (`~/.costcutter.{yaml,yml,toml,json}`)
1111
3. **Explicit File** - File specified via `--config` flag or `config_file` parameter
1212
4. **Environment Variables** - Shell environment variables with `COSTCUTTER_` prefix
@@ -18,7 +18,7 @@ CostCutter merges configuration from multiple sources in this order (later sourc
1818

1919
### Method 1: Default Configuration (Built-in)
2020

21-
Located at `src/costcutter/conf/config.yaml`. Used automatically if no other config is provided.
21+
Defined in `src/costcutter/config.py`. Used automatically if no other config is provided.
2222

2323
**Default Configuration:**
2424

@@ -114,9 +114,9 @@ costcutter --config /path/to/myconfig.yaml
114114

115115
```python
116116
from pathlib import Path
117-
from costcutter.conf.config import get_config
117+
from costcutter.config import load_config
118118
119-
config = get_config(config_file=Path("/path/to/myconfig.yaml"))
119+
config = load_config()
120120
```
121121

122122
**Supported formats:** `.yaml`, `.yml`, `.toml`, `.json`
@@ -177,12 +177,12 @@ export COSTCUTTER_AWS__SERVICES="[ec2, s3]" # List
177177

178178
```python
179179
import os
180-
from costcutter.conf.config import get_config
180+
from costcutter.config import load_config
181181
182182
os.environ["COSTCUTTER_DRY_RUN"] = "false"
183183
os.environ["COSTCUTTER_AWS__REGION"] = "[us-east-1]"
184184
185-
config = get_config()
185+
config = load_config()
186186
```
187187

188188
**Priority:** Overrides defaults, home config, and explicit files. Only CLI arguments have higher priority.
@@ -207,7 +207,7 @@ costcutter --config myconfig.yaml --dry-run
207207
**Python API:**
208208

209209
```python
210-
from costcutter.conf.config import get_config
210+
from costcutter.config import load_config
211211
212212
cli_overrides = {
213213
"dry_run": False,
@@ -216,7 +216,7 @@ cli_overrides = {
216216
}
217217
}
218218
219-
config = get_config(cli_args=cli_overrides)
219+
config = load_config(overrides=cli_overrides)
220220
```
221221

222222
**Priority:** Highest priority. Overrides all other sources.
@@ -228,11 +228,11 @@ Import and configure programmatically:
228228
### Basic Usage
229229

230230
```python
231-
from costcutter.conf.config import get_config
231+
from costcutter.config import load_config
232232
from costcutter.orchestrator import orchestrate_services
233233
234234
# Load default config
235-
config = get_config()
235+
config = load_config()
236236
237237
# Run cleanup
238238
orchestrate_services(dry_run=True)
@@ -241,53 +241,42 @@ orchestrate_services(dry_run=True)
241241
### Custom Configuration
242242

243243
```python
244-
from pathlib import Path
245-
from costcutter.conf.config import get_config
244+
from costcutter.config import load_config
246245
247-
# Method 1: Use config file
248-
config = get_config(config_file=Path("./myconfig.yaml"))
246+
# Method 1: Load with defaults (auto-discovers config files)
247+
config = load_config()
249248
250-
# Method 2: Override via CLI args
251-
config = get_config(cli_args={
249+
# Method 2: Override via runtime parameters
250+
config = load_config(overrides={
252251
"dry_run": False,
253252
"aws": {
254253
"region": ["us-west-2"],
255254
"services": ["ec2", "s3"]
256255
}
257256
})
258-
259-
# Method 3: Combine both
260-
config = get_config(
261-
config_file=Path("./base.yaml"),
262-
cli_args={"dry_run": False}
263-
)
264257
```
265258

266259
### Reload Configuration
267260

268261
```python
269-
from costcutter.conf.config import reload_config
262+
from costcutter.config import load_config
270263
271264
# Reload with new settings
272-
config = reload_config(cli_args={"dry_run": False})
265+
config = load_config(overrides={"dry_run": False})
273266
```
274267

275268
### Access Configuration Values
276269

277270
```python
278-
config = get_config()
271+
config = load_config()
279272
280273
# Dot notation
281274
print(config.dry_run)
282275
print(config.aws.region)
283276
print(config.logging.level)
284277
285-
# Dictionary notation
286-
print(config["dry_run"])
287-
print(config["aws"]["region"])
288-
289278
# Convert to dict
290-
config_dict = config.to_dict()
279+
config_dict = config.model_dump()
291280
print(config_dict["aws"]["services"])
292281
```
293282

@@ -525,22 +514,18 @@ costcutter
525514

526515
```python
527516
# cleanup.py
528-
from pathlib import Path
529-
from costcutter.conf.config import get_config
517+
from costcutter.config import load_config
530518
from costcutter.orchestrator import orchestrate_services
531519
from costcutter.logger import setup_logging
532520
533-
# Load configuration
534-
config = get_config(
535-
config_file=Path("./config.yaml"),
536-
cli_args={
537-
"dry_run": False,
538-
"aws": {
539-
"region": ["us-east-1"],
540-
"services": ["ec2"]
541-
}
521+
# Load configuration with overrides
522+
config = load_config(overrides={
523+
"dry_run": False,
524+
"aws": {
525+
"region": ["us-east-1"],
526+
"services": ["ec2"]
542527
}
543-
)
528+
})
544529
545530
# Setup logging
546531
setup_logging(config)
@@ -644,10 +629,10 @@ costcutter --config myconfig.txt
644629
**Debug configuration:**
645630

646631
```python
647-
from costcutter.conf.config import get_config
632+
from costcutter.config import load_config
648633
649-
config = get_config()
650-
print(config.to_dict()) # See final merged config
634+
config = load_config()
635+
print(config.model_dump()) # See final merged config
651636
```
652637

653638
### Environment variables not working
@@ -662,7 +647,7 @@ print(config.to_dict()) # See final merged config
662647

663648
```bash
664649
export COSTCUTTER_DRY_RUN=false
665-
python -c "import os; from costcutter.conf.config import get_config; print(get_config().dry_run)"
650+
python -c "import os; from costcutter.config import load_config; print(load_config().dry_run)"
666651
# Should print: False
667652
```
668653

docs/guide/how-it-works.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This page breaks down a single CostCutter run from CLI invocation to final summa
55
## High level timeline
66

77
1. **CLI start** - Typer parses flags, clears the console, and prints the ASCII banner
8-
2. **Configuration merge** - `get_config` loads defaults, home overrides, optional files, environment variables, and CLI overrides
8+
2. **Configuration merge** - `load_config` loads defaults, home overrides, optional files, environment variables, and CLI overrides
99
3. **Logging** - `setup_logging` configures file handlers and suppresses noisy libraries
1010
4. **Session creation** - `create_aws_session` builds a boto3 session using explicit keys, a credential file, or the default resolver
1111
5. **Worklist build** - the orchestrator pairs each configured region with each requested service and filters out unsupported region/service combinations
@@ -37,7 +37,7 @@ This page breaks down a single CostCutter run from CLI invocation to final summa
3737

3838
Configuration sources apply in this order:
3939

40-
1. Bundled defaults (`src/costcutter/conf/config.yaml`)
40+
1. Built-in defaults (defined in `src/costcutter/config.py`)
4141
2. Home overrides (`~/.costcutter.yaml`, `.yml`, `.toml`, `.json`)
4242
3. Explicit file from `--config`
4343
4. Environment variables prefixed with `COSTCUTTER_`

docs/usage-cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ All modes expose the same options.
1818
| `--no-dry-run` | Disable dry run for the current invocation. |
1919
| `--config PATH` | Load an explicit YAML, YML, TOML, or JSON file. |
2020

21-
Dry run is enabled by default through `src/costcutter/conf/config.yaml`. Use `--no-dry-run` only when you intend to delete resources.
21+
Dry run is enabled by default. Use `--no-dry-run` only when you intend to delete resources.
2222

2323
## Typical Workflows
2424

src/costcutter/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from costcutter.conf.config import load_config
1+
from costcutter.config import load_config
22
from costcutter.logger import setup_logging
33
from costcutter.main import run
44

src/costcutter/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from rich.live import Live
1212
from rich.table import Table
1313

14-
from costcutter.conf.config import load_config
14+
from costcutter.config import load_config
1515
from costcutter.logger import setup_logging
1616
from costcutter.orchestrator import orchestrate_services
1717
from costcutter.reporter import get_reporter

src/costcutter/conf/__init__.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/costcutter/core/session_helper.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,26 @@
11
import logging
22
import os
3-
from typing import Any
43

54
import boto3
65
from boto3.session import Session
7-
from pydantic import BaseModel
86

9-
from costcutter.conf.config import Config
7+
from costcutter.config import Config
108

119
logger = logging.getLogger(__name__)
1210

1311

14-
def _get_aws_value(aws_config: BaseModel | dict[str, Any] | object, key: str, default: str | None = None) -> str | None:
15-
"""Extract value from aws config (BaseModel, dict, or object)."""
16-
if isinstance(aws_config, BaseModel):
17-
return getattr(aws_config, key, default)
18-
if isinstance(aws_config, dict):
19-
return aws_config.get(key, default)
20-
return getattr(aws_config, key, default)
21-
22-
2312
def create_aws_session(config: Config) -> Session:
2413
"""Create a boto3 Session based on aws-related settings in Config.
2514
2615
Falls back through explicit keys, credential file, then default discovery.
2716
"""
28-
try:
29-
aws_config = config.aws
30-
except AttributeError:
31-
aws_config = {}
32-
33-
# Handle dict-based aws_config
34-
aws_access_key_id = _get_aws_value(aws_config, "aws_access_key_id")
35-
aws_secret_access_key = _get_aws_value(aws_config, "aws_secret_access_key")
36-
aws_session_token = _get_aws_value(aws_config, "aws_session_token")
37-
credential_file_path_raw = _get_aws_value(aws_config, "credential_file_path", "")
38-
credential_file_path = os.path.expanduser(credential_file_path_raw or "")
39-
profile_name = _get_aws_value(aws_config, "profile", "default")
17+
aws_config = config.aws
18+
19+
aws_access_key_id = aws_config.aws_access_key_id
20+
aws_secret_access_key = aws_config.aws_secret_access_key
21+
aws_session_token = aws_config.aws_session_token
22+
credential_file_path = os.path.expanduser(aws_config.credential_file_path or "")
23+
profile_name = aws_config.profile
4024

4125
if aws_access_key_id and aws_secret_access_key:
4226
logger.info("Using credentials from config (access key + secret)")

0 commit comments

Comments
 (0)