dlab-cli 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.
dlab/config.py ADDED
@@ -0,0 +1,190 @@
1
+ """
2
+ Configuration loading and validation for decision-pack config directories.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+
11
+ REQUIRED_DIRS: list[str] = ["docker", "opencode"]
12
+ REQUIRED_FILES: list[str] = ["config.yaml"]
13
+ CONFIG_KEYS: list[str] = ["name", "description", "docker_image_name", "default_model"]
14
+
15
+
16
+ def list_config_issues(config_dir: str) -> list[str]:
17
+ """
18
+ Check a decision-pack directory and return a list of issues found.
19
+
20
+ Parameters
21
+ ----------
22
+ config_dir : str
23
+ Path to the decision-pack config directory.
24
+
25
+ Returns
26
+ -------
27
+ list[str]
28
+ List of issue descriptions. Empty if valid.
29
+ """
30
+ issues: list[str] = []
31
+ config_path: Path = Path(config_dir)
32
+
33
+ if not config_path.exists():
34
+ return [f"Directory does not exist: {config_dir}"]
35
+ if not config_path.is_dir():
36
+ return [f"Path is not a directory: {config_dir}"]
37
+
38
+ for required_dir in REQUIRED_DIRS:
39
+ dir_path: Path = config_path / required_dir
40
+ if not dir_path.exists():
41
+ issues.append(f"Missing directory: {required_dir}/")
42
+ elif not dir_path.is_dir():
43
+ issues.append(f"Expected directory but found file: {required_dir}")
44
+
45
+ for required_file in REQUIRED_FILES:
46
+ file_path: Path = config_path / required_file
47
+ if not file_path.exists():
48
+ issues.append(f"Missing file: {required_file}")
49
+ elif not file_path.is_file():
50
+ issues.append(f"Expected file but found directory: {required_file}")
51
+
52
+ return issues
53
+
54
+
55
+ def validate_config_structure(config_dir: str) -> None:
56
+ """
57
+ Validate that a decision-pack config directory has the required structure.
58
+
59
+ Parameters
60
+ ----------
61
+ config_dir : str
62
+ Path to the decision-pack config directory.
63
+
64
+ Raises
65
+ ------
66
+ ValueError
67
+ If the directory structure is invalid.
68
+ """
69
+ config_path: Path = Path(config_dir)
70
+
71
+ if not config_path.exists():
72
+ raise ValueError(f"Config directory does not exist: {config_dir}")
73
+
74
+ if not config_path.is_dir():
75
+ raise ValueError(f"Config path is not a directory: {config_dir}")
76
+
77
+ for required_dir in REQUIRED_DIRS:
78
+ dir_path: Path = config_path / required_dir
79
+ if not dir_path.exists():
80
+ raise ValueError(f"Missing required directory: {required_dir}")
81
+ if not dir_path.is_dir():
82
+ raise ValueError(f"Expected directory but found file: {required_dir}")
83
+
84
+ for required_file in REQUIRED_FILES:
85
+ file_path: Path = config_path / required_file
86
+ if not file_path.exists():
87
+ raise ValueError(f"Missing required file: {required_file}")
88
+ if not file_path.is_file():
89
+ raise ValueError(f"Expected file but found directory: {required_file}")
90
+
91
+
92
+ def load_config_yaml(config_dir: str) -> dict[str, Any]:
93
+ """
94
+ Load and validate config.yaml from a decision-pack config directory.
95
+
96
+ Parameters
97
+ ----------
98
+ config_dir : str
99
+ Path to the decision-pack config directory.
100
+
101
+ Returns
102
+ -------
103
+ dict[str, Any]
104
+ The parsed config.yaml contents.
105
+
106
+ Raises
107
+ ------
108
+ ValueError
109
+ If config.yaml is invalid or missing required keys.
110
+ """
111
+ config_path: Path = Path(config_dir) / "config.yaml"
112
+
113
+ try:
114
+ with open(config_path, "r") as f:
115
+ config: dict[str, Any] = yaml.safe_load(f)
116
+ except yaml.YAMLError as e:
117
+ raise ValueError(f"Invalid YAML in config.yaml: {e}")
118
+
119
+ if not isinstance(config, dict):
120
+ raise ValueError("config.yaml must contain a YAML mapping")
121
+
122
+ missing_keys: list[str] = [key for key in CONFIG_KEYS if key not in config]
123
+ if missing_keys:
124
+ raise ValueError(f"config.yaml missing required keys: {missing_keys}")
125
+
126
+ return config
127
+
128
+
129
+ def load_dpack_config(config_dir: str) -> dict[str, Any]:
130
+ """
131
+ Load and validate a complete decision-pack configuration.
132
+
133
+ Parameters
134
+ ----------
135
+ config_dir : str
136
+ Path to the decision-pack config directory.
137
+
138
+ Returns
139
+ -------
140
+ dict[str, Any]
141
+ Complete decision-pack configuration including:
142
+ - config_dir: Absolute path to config directory
143
+ - name: decision-pack name
144
+ - description: decision-pack description
145
+ - docker_image_name: Name for the Docker image
146
+ - default_model: Default LLM model to use
147
+ - opencode_version: Version of opencode to install (optional, defaults to "latest")
148
+
149
+ Raises
150
+ ------
151
+ ValueError
152
+ If the configuration is invalid.
153
+ """
154
+ config_path: Path = Path(config_dir).resolve()
155
+ config_dir_str: str = str(config_path)
156
+
157
+ validate_config_structure(config_dir_str)
158
+ config: dict[str, Any] = load_config_yaml(config_dir_str)
159
+
160
+ config["config_dir"] = config_dir_str
161
+
162
+ # Autodetect package_manager from docker/ contents if not specified
163
+ if "package_manager" not in config:
164
+ docker_dir: Path = config_path / "docker"
165
+ if (docker_dir / "environment.yml").exists():
166
+ config["package_manager"] = "conda"
167
+ elif (docker_dir / "pixi.toml").exists():
168
+ config["package_manager"] = "pixi"
169
+ else:
170
+ config["package_manager"] = "pip"
171
+
172
+ # Default opencode_version to "latest" if not specified
173
+ if "opencode_version" not in config:
174
+ config["opencode_version"] = "latest"
175
+
176
+ # Normalize hooks: string -> list, missing -> empty list
177
+ hooks: dict[str, Any] = config.get("hooks", {})
178
+ if not isinstance(hooks, dict):
179
+ hooks = {}
180
+ for key in ("pre-run", "post-run"):
181
+ value: Any = hooks.get(key, [])
182
+ if isinstance(value, str):
183
+ hooks[key] = [value]
184
+ elif isinstance(value, list):
185
+ hooks[key] = value
186
+ else:
187
+ hooks[key] = []
188
+ config["hooks"] = hooks
189
+
190
+ return config