Skip to content

Commit c62900f

Browse files
authored
Merge pull request #16 from SurgeCLI/feature/14-config-setup
Feature/14 config setup
2 parents 054be09 + f8dbea1 commit c62900f

9 files changed

Lines changed: 197 additions & 2 deletions

File tree

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.github/
2+
.pytest_cache/
3+
.ruff_cache/
4+
.gitignore
5+
__pycache__/
6+
venv/

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ COPY cli/requirements.txt .
88
RUN pip install --no-cache-dir -r requirements.txt
99
RUN pip install --no-cache-dir pytest
1010

11-
COPY cli /app/cli
11+
COPY . /app
1212

13-
ENTRYPOINT ["python", "cli/app.py"]
13+
ENTRYPOINT ["python", "-m", "cli.app"]
1414
CMD [""]

cli/__init__.py

Whitespace-only changes.

cli/app.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
import os
22
import subprocess
33
import typer
4+
5+
from pathlib import Path
46
from rich import print
57
from typing import Annotated
68

9+
from config import config
10+
from .merge import merge
11+
12+
try:
13+
config_data = config.load_config_file(Path("config/config.toml"))
14+
except Exception:
15+
config_data = {}
16+
print(f"Check that a config.toml file is populated here: '{Path.home()}'")
17+
print("Common Problems: an API key is not set, or is invalid.")
718

819
app = typer.Typer(
920
help="Surge - A DevOps CLI Tool For System Monitoring and Production Reliability"
1021
)
1122

23+
# Merges app.command() decorator w/ transposed merge() decorator
24+
cmd = app.command
25+
26+
27+
def app_command_with_merge(*args, **kwargs):
28+
decorator = cmd(*args, **kwargs)
29+
30+
def wrapper(func):
31+
return decorator(merge()(func))
32+
33+
return wrapper
34+
35+
36+
app.command = app_command_with_merge
37+
1238

1339
def run_cmd(cmd: str) -> str:
1440
"""

cli/merge.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import inspect
2+
from functools import wraps
3+
4+
5+
def merge(section: str | None = None):
6+
"""
7+
Decorator to merge the func call arguments/options of Typer with config defaults provided via TOML tables.
8+
The precedence is as follows:
9+
- Explicit CLI Arguments: (i.e. surge <cmd> -<option-flag> --<option-name>)
10+
11+
- Configuration Data for a given section (
12+
found in TOML table under table, i.e.:
13+
[table-name]
14+
<option> = <value>
15+
16+
# Provided as a KWArg for Python in config.py
17+
)
18+
19+
- Typer function default signature (
20+
i.e.
21+
def name(option: typer.Option('-x', '--flag') = <value>, ...):
22+
)
23+
"""
24+
25+
def decorator(func):
26+
orig_sig = inspect.signature(func)
27+
declared_defaults = {
28+
name: param.default
29+
for name, param in orig_sig.parameters.items()
30+
if param.default is not inspect._empty
31+
}
32+
33+
# Modded signature with defaults set to None for Typer to see as optional; leaves *args, **kwargs, and positional-only params default
34+
new_params = []
35+
for param in orig_sig.parameters.values():
36+
if param.kind in (
37+
inspect.Parameter.VAR_POSITIONAL,
38+
inspect.Parameter.VAR_KEYWORD,
39+
inspect.Parameter.POSITIONAL_ONLY,
40+
):
41+
new_params.append(param)
42+
else:
43+
new_params.append(param.replace(default=None))
44+
new_sig = orig_sig.replace(parameters=new_params)
45+
46+
# Actual wrapper lets go
47+
@wraps(func)
48+
def wrapper(*args, **kwargs):
49+
config_data = func.__globals__.get("config_data", {})
50+
51+
section_key = section or func.__name__
52+
config_section = (
53+
config_data.get(section_key, {})
54+
if isinstance(config_data, dict)
55+
else {}
56+
)
57+
58+
bound = orig_sig.bind_partial(*args, **kwargs)
59+
final_args = {}
60+
61+
for name in orig_sig.parameters:
62+
val = bound.arguments.get(name, None)
63+
64+
if val is not None: # Explicit cli option
65+
final_args[name] = val
66+
continue
67+
68+
if name in config_section: # Config option
69+
final_args[name] = config_section[name]
70+
continue
71+
72+
if name in declared_defaults: # Default fallback
73+
final_args[name] = declared_defaults[name]
74+
else:
75+
final_args[name] = None
76+
77+
return func(**final_args)
78+
79+
wrapper.__signature__ = new_sig
80+
return wrapper
81+
82+
return decorator

cli/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ shellingham
4848
sniffio
4949
SQLAlchemy
5050
tenacity
51+
tomli-w
5152
typer
5253
typing-inspection
5354
typing_extensions

config/__init__.py

Whitespace-only changes.

config/config.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import tomllib
2+
import tomli_w
3+
from pathlib import Path
4+
5+
PATH = Path("config/config.toml")
6+
7+
DEFAULT_DATA = {
8+
"console": {"force_color": True, "theme": "default"},
9+
"monitor": {
10+
"interval": 5,
11+
"load": True,
12+
"cpu": True,
13+
"ram": True,
14+
"disk": True,
15+
"io": False,
16+
"verbose": False,
17+
},
18+
"network": {"requests": 5, "dtype": "A", "sockets": False, "no_trace": False},
19+
"ai": {"format": "hybrid", "verbosity": "normal", "auto_fix": False},
20+
}
21+
22+
23+
def create_config_file(path: Path) -> None:
24+
try:
25+
with open(path, "wb") as config:
26+
tomli_w.dump(DEFAULT_DATA, config)
27+
print(f"Created config file at {Path.home()}")
28+
except Exception as e:
29+
print(f"Could not create config.toml file: {e}")
30+
31+
32+
def load_config_file(path: Path) -> dict:
33+
if not path.exists():
34+
print("Config file does not exist, creating new config file...")
35+
create_config_file(path)
36+
return DEFAULT_DATA
37+
38+
with open(path, "rb") as config:
39+
data = tomllib.load(config)
40+
41+
if not data:
42+
print("Config file is empty, creating new config file...")
43+
create_config_file(path)
44+
return DEFAULT_DATA
45+
46+
return data
47+
48+
49+
if __name__ == "__main__":
50+
"""
51+
Run this file for a default config.toml file
52+
"""
53+
try:
54+
load_config_file(PATH)
55+
print(f"Loaded config file at {PATH}")
56+
except Exception:
57+
create_config_file()

config/config.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[console]
2+
force_color = true
3+
theme = "default"
4+
5+
[monitor]
6+
interval = 5
7+
load = true
8+
cpu = true
9+
ram = true
10+
disk = true
11+
io = false
12+
verbose = false
13+
14+
[network]
15+
requests = 5
16+
dtype = "A"
17+
sockets = false
18+
no_trace = false
19+
20+
[ai]
21+
format = "hybrid"
22+
verbosity = "normal"
23+
auto_fix = false

0 commit comments

Comments
 (0)