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.
- rhylthyme_cli_runner/__init__.py +15 -0
- rhylthyme_cli_runner/cli.py +273 -0
- rhylthyme_cli_runner/environment_icons.py +258 -0
- rhylthyme_cli_runner/environment_loader.py +293 -0
- rhylthyme_cli_runner/environment_schemas.py +443 -0
- rhylthyme_cli_runner/program_planner.py +871 -0
- rhylthyme_cli_runner/program_runner.py +2257 -0
- rhylthyme_cli_runner/validate_program.py +457 -0
- rhylthyme_cli_runner-0.1.0a0.dist-info/METADATA +268 -0
- rhylthyme_cli_runner-0.1.0a0.dist-info/RECORD +13 -0
- rhylthyme_cli_runner-0.1.0a0.dist-info/WHEEL +5 -0
- rhylthyme_cli_runner-0.1.0a0.dist-info/entry_points.txt +2 -0
- rhylthyme_cli_runner-0.1.0a0.dist-info/top_level.txt +1 -0
|
@@ -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)
|