rhylthyme-cli-runner 0.1.0a0__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,293 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Environment Loader Module
4
+
5
+ This module handles loading environment catalogs and providing resource constraints
6
+ for programs that reference environments instead of embedding constraints directly.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import yaml
12
+ from typing import Dict, List, Any, Optional
13
+ from pathlib import Path
14
+
15
+
16
+ class EnvironmentLoader:
17
+ """Handles loading and managing environment catalogs."""
18
+
19
+ def __init__(self, environments_dir: str = None):
20
+ """
21
+ Initialize the environment loader.
22
+
23
+ Args:
24
+ environments_dir: Path to the environments directory.
25
+ Defaults to 'environments' in the project root.
26
+ """
27
+ if environments_dir is None:
28
+ # Default to environments directory relative to this file
29
+ module_dir = Path(__file__).parent.parent.parent
30
+ environments_dir = module_dir / "environments"
31
+
32
+ self.environments_dir = Path(environments_dir)
33
+ self._cache = {}
34
+
35
+ def load_environment(self, environment_id: str) -> Dict[str, Any]:
36
+ """
37
+ Load an environment catalog by ID.
38
+
39
+ Args:
40
+ environment_id: The ID of the environment to load
41
+
42
+ Returns:
43
+ The environment catalog data
44
+
45
+ Raises:
46
+ FileNotFoundError: If the environment file is not found
47
+ ValueError: If the environment file is invalid
48
+ """
49
+ # Check cache first
50
+ if environment_id in self._cache:
51
+ return self._cache[environment_id]
52
+
53
+ # Try to find the environment file
54
+ environment_file = None
55
+
56
+ # First try direct file name
57
+ for ext in ['.json', '.yaml', '.yml']:
58
+ potential_file = self.environments_dir / f"{environment_id}{ext}"
59
+ if potential_file.exists():
60
+ environment_file = potential_file
61
+ break
62
+
63
+ # If not found, scan all files for matching environmentId
64
+ if environment_file is None:
65
+ for file_path in self.environments_dir.glob("*"):
66
+ if file_path.suffix in ['.json', '.yaml', '.yml']:
67
+ try:
68
+ data = self._load_file(file_path)
69
+ if data.get('environmentId') == environment_id:
70
+ environment_file = file_path
71
+ break
72
+ except:
73
+ continue
74
+
75
+ if environment_file is None:
76
+ raise FileNotFoundError(f"Environment '{environment_id}' not found in {self.environments_dir}")
77
+
78
+ # Load the environment data
79
+ environment_data = self._load_file(environment_file)
80
+
81
+ # Validate required fields
82
+ if 'environmentId' not in environment_data:
83
+ raise ValueError(f"Environment file {environment_file} missing required 'environmentId' field")
84
+
85
+ if 'resourceConstraints' not in environment_data:
86
+ raise ValueError(f"Environment file {environment_file} missing required 'resourceConstraints' field")
87
+
88
+ # Cache the loaded environment
89
+ self._cache[environment_id] = environment_data
90
+
91
+ return environment_data
92
+
93
+ def _load_file(self, file_path: Path) -> Dict[str, Any]:
94
+ """Load a JSON or YAML file."""
95
+ with open(file_path, 'r') as f:
96
+ if file_path.suffix == '.json':
97
+ return json.load(f)
98
+ else: # .yaml or .yml
99
+ return yaml.safe_load(f)
100
+
101
+ def get_resource_constraints(self, environment_id: str) -> List[Dict[str, Any]]:
102
+ """
103
+ Get resource constraints for a specific environment.
104
+
105
+ Args:
106
+ environment_id: The ID of the environment
107
+
108
+ Returns:
109
+ List of resource constraint definitions
110
+ """
111
+ environment = self.load_environment(environment_id)
112
+ return environment.get('resourceConstraints', [])
113
+
114
+ def get_environment(self, environment_id: str) -> Optional[Dict[str, Any]]:
115
+ """
116
+ Get the full environment data for a specific environment.
117
+
118
+ Args:
119
+ environment_id: The ID of the environment
120
+
121
+ Returns:
122
+ The full environment data or None if not found
123
+ """
124
+ try:
125
+ return self.load_environment(environment_id)
126
+ except (FileNotFoundError, ValueError):
127
+ return None
128
+
129
+ def list_environments(self) -> List[Dict[str, str]]:
130
+ """
131
+ List all available environments.
132
+
133
+ Returns:
134
+ List of environment summaries with id, name, type, description, and icon
135
+ """
136
+ environments = []
137
+
138
+ if not self.environments_dir.exists():
139
+ return environments
140
+
141
+ for file_path in sorted(self.environments_dir.glob("*")):
142
+ if file_path.suffix in ['.json', '.yaml', '.yml']:
143
+ try:
144
+ data = self._load_file(file_path)
145
+ if 'environmentId' in data:
146
+ env_type = data.get('type', 'Unknown')
147
+
148
+ # Get icon for environment type
149
+ try:
150
+ from .environment_icons import get_environment_icon
151
+ icon = get_environment_icon(env_type)
152
+ except ImportError:
153
+ icon = 'fa-building' # Default fallback
154
+
155
+ environments.append({
156
+ 'id': data['environmentId'],
157
+ 'name': data.get('name', 'Unknown'),
158
+ 'type': env_type,
159
+ 'description': data.get('description', ''),
160
+ 'icon': icon
161
+ })
162
+ except:
163
+ continue
164
+
165
+ return environments
166
+
167
+ def list_environments_by_type(self, environment_type: str) -> List[Dict[str, str]]:
168
+ """
169
+ List all environments that match a specific type.
170
+
171
+ Args:
172
+ environment_type: The type of environment to filter by (e.g., 'kitchen', 'laboratory')
173
+
174
+ Returns:
175
+ List of environment summaries matching the type
176
+ """
177
+ all_environments = self.list_environments()
178
+ return [env for env in all_environments if env['type'] == environment_type]
179
+
180
+ def find_suitable_environments(self, program_data: Dict[str, Any]) -> List[Dict[str, str]]:
181
+ """
182
+ Find environments suitable for a program based on its environmentType.
183
+
184
+ Args:
185
+ program_data: The program data containing environmentType
186
+
187
+ Returns:
188
+ List of suitable environment summaries
189
+ """
190
+ environment_type = program_data.get('environmentType')
191
+ if not environment_type:
192
+ return self.list_environments()
193
+
194
+ return self.list_environments_by_type(environment_type)
195
+
196
+ def get_default_environment_for_type(self, environment_type: str) -> Optional[str]:
197
+ """
198
+ Get the default environment ID for a given type.
199
+
200
+ Args:
201
+ environment_type: The type of environment
202
+
203
+ Returns:
204
+ The ID of the default environment for this type, or None if no suitable environment found
205
+ """
206
+ environments = self.list_environments_by_type(environment_type)
207
+ if not environments:
208
+ return None
209
+
210
+ # Prefer environments with 'standard' in the name, otherwise use the first one
211
+ for env in environments:
212
+ if 'standard' in env['id'].lower() or 'default' in env['id'].lower():
213
+ return env['id']
214
+
215
+ return environments[0]['id']
216
+
217
+ def merge_constraints(self, program_constraints: List[Dict[str, Any]],
218
+ environment_id: Optional[str]) -> List[Dict[str, Any]]:
219
+ """
220
+ Merge program-specific constraints with environment constraints.
221
+
222
+ Program-specific constraints take precedence over environment constraints.
223
+
224
+ Args:
225
+ program_constraints: Resource constraints defined in the program
226
+ environment_id: The environment ID to load constraints from
227
+
228
+ Returns:
229
+ Merged list of resource constraints
230
+ """
231
+ if environment_id is None:
232
+ return program_constraints
233
+
234
+ # Load environment constraints
235
+ try:
236
+ env_constraints = self.get_resource_constraints(environment_id)
237
+ except (FileNotFoundError, ValueError):
238
+ # If environment not found, just use program constraints
239
+ return program_constraints
240
+
241
+ # Create a map of program constraints by task
242
+ program_map = {c['task']: c for c in program_constraints}
243
+
244
+ # Start with environment constraints
245
+ merged = []
246
+ for env_constraint in env_constraints:
247
+ task = env_constraint['task']
248
+ if task in program_map:
249
+ # Program constraint overrides environment
250
+ merged.append(program_map[task])
251
+ del program_map[task]
252
+ else:
253
+ # Use environment constraint
254
+ merged.append(env_constraint)
255
+
256
+ # Add any remaining program constraints
257
+ merged.extend(program_map.values())
258
+
259
+ return merged
260
+
261
+
262
+ # Global instance for convenient access
263
+ _default_loader = None
264
+
265
+
266
+ def get_default_loader() -> EnvironmentLoader:
267
+ """Get the default environment loader instance."""
268
+ global _default_loader
269
+ if _default_loader is None:
270
+ _default_loader = EnvironmentLoader()
271
+ return _default_loader
272
+
273
+
274
+ def load_resource_constraints(program_data: Dict[str, Any]) -> List[Dict[str, Any]]:
275
+ """
276
+ Load resource constraints for a program, handling both embedded and environment-based constraints.
277
+
278
+ Args:
279
+ program_data: The program data dictionary
280
+
281
+ Returns:
282
+ List of resource constraints
283
+ """
284
+ loader = get_default_loader()
285
+
286
+ # Get constraints from the program (if any)
287
+ program_constraints = program_data.get('resourceConstraints', [])
288
+
289
+ # Get environment ID (if any)
290
+ environment_id = program_data.get('environment')
291
+
292
+ # Merge constraints
293
+ return loader.merge_constraints(program_constraints, environment_id)