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,443 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Environment Schema Validation System
4
+
5
+ This module provides schemas and validation for environment catalog files,
6
+ ensuring they conform to the expected structure for their environment type.
7
+ """
8
+
9
+ import json
10
+ import jsonschema
11
+ from typing import Dict, List, Any, Optional, Set
12
+ from pathlib import Path
13
+
14
+ # Base environment schema that all environments must conform to
15
+ BASE_ENVIRONMENT_SCHEMA = {
16
+ "$schema": "http://json-schema.org/draft-07/schema#",
17
+ "title": "Base Environment Schema",
18
+ "description": "Base schema for environment catalog files",
19
+ "type": "object",
20
+ "required": ["environmentId", "name", "type", "resourceConstraints"],
21
+ "properties": {
22
+ "environmentId": {
23
+ "type": "string",
24
+ "description": "Unique identifier for the environment",
25
+ "pattern": "^[a-z0-9-]+$"
26
+ },
27
+ "name": {
28
+ "type": "string",
29
+ "description": "Human-readable name for the environment"
30
+ },
31
+ "description": {
32
+ "type": "string",
33
+ "description": "Detailed description of the environment"
34
+ },
35
+ "type": {
36
+ "type": "string",
37
+ "description": "Type of environment (e.g., kitchen, laboratory, bakery, airport)",
38
+ "enum": [
39
+ "kitchen", "laboratory", "bakery", "airport", "restaurant",
40
+ "home", "commercial-kitchen", "lab", "research", "biotech",
41
+ "pharma", "medical", "artisan", "pastry", "aviation",
42
+ "runway", "terminal", "manufacturing", "warehouse", "office",
43
+ "hospital", "school", "retail", "farm", "datacenter",
44
+ "factory", "workshop", "garage", "gym", "studio", "theater",
45
+ "library", "garden", "greenhouse", "clinic", "spa", "hotel"
46
+ ]
47
+ },
48
+ "actors": {
49
+ "type": "integer",
50
+ "minimum": 1,
51
+ "description": "Legacy: Total number of actors (use actorTypes for new environments)"
52
+ },
53
+ "actorTypes": {
54
+ "type": "object",
55
+ "description": "Definition of different actor types and their counts",
56
+ "patternProperties": {
57
+ "^[a-z0-9-]+$": {
58
+ "type": "object",
59
+ "required": ["name", "count"],
60
+ "properties": {
61
+ "name": {
62
+ "type": "string",
63
+ "description": "Human-readable name for the actor type"
64
+ },
65
+ "count": {
66
+ "type": "integer",
67
+ "minimum": 1,
68
+ "description": "Number of actors of this type"
69
+ },
70
+ "description": {
71
+ "type": "string",
72
+ "description": "Description of the actor type's role"
73
+ },
74
+ "qualifications": {
75
+ "type": "array",
76
+ "items": {"type": "string"},
77
+ "description": "List of qualifications this actor type possesses"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ },
83
+ "resourceConstraints": {
84
+ "type": "array",
85
+ "description": "List of resource constraints for this environment",
86
+ "minItems": 1,
87
+ "items": {
88
+ "type": "object",
89
+ "required": ["task", "maxConcurrent", "description"],
90
+ "properties": {
91
+ "task": {
92
+ "type": "string",
93
+ "description": "Name of the task/resource type"
94
+ },
95
+ "maxConcurrent": {
96
+ "type": "integer",
97
+ "minimum": 1,
98
+ "description": "Maximum number of concurrent uses of this resource"
99
+ },
100
+ "actorsRequired": {
101
+ "type": "number",
102
+ "minimum": 0,
103
+ "description": "Number of actors required for this task"
104
+ },
105
+ "qualifiedActorTypes": {
106
+ "type": "array",
107
+ "items": {"type": "string"},
108
+ "description": "Actor types qualified to perform this task"
109
+ },
110
+ "description": {
111
+ "type": "string",
112
+ "description": "Description of the resource or equipment"
113
+ }
114
+ }
115
+ }
116
+ },
117
+ "metadata": {
118
+ "type": "object",
119
+ "description": "Additional metadata about the environment"
120
+ }
121
+ },
122
+ "anyOf": [
123
+ {"required": ["actors"]},
124
+ {"required": ["actorTypes"]}
125
+ ]
126
+ }
127
+
128
+ # Type-specific task definitions for different environment types
129
+ ENVIRONMENT_TYPE_TASKS = {
130
+ "kitchen": {
131
+ "required_tasks": ["stove-burner", "prep-work", "cleanup"],
132
+ "common_tasks": [
133
+ "stove-burner", "oven", "microwave", "toaster", "prep-work",
134
+ "plating", "cleanup", "mixer", "refrigerator", "dishwasher",
135
+ "grill", "fryer", "steamer", "food-processor", "walk-in-cooler",
136
+ "freezer", "expediting", "service-window", "prep-station",
137
+ "cold-prep", "grill-station", "pastry-oven", "dishwashing"
138
+ ],
139
+ "actor_types": [
140
+ "home-cook", "chef", "cook", "prep-cook", "head-chef",
141
+ "sous-chef", "line-cook", "dishwasher", "chef-de-partie",
142
+ "commis-chef", "kitchen-porter", "pastry-chef", "expediter"
143
+ ]
144
+ },
145
+ "laboratory": {
146
+ "required_tasks": ["bench-space", "cleanup"],
147
+ "common_tasks": [
148
+ "fume-hood", "biosafety-cabinet", "incubator", "centrifuge",
149
+ "microscope", "spectrophotometer", "pcr-machine", "hplc",
150
+ "mass-spectrometer", "flow-cytometer", "bench-space",
151
+ "prep-work", "autoclave", "freezer-80", "liquid-nitrogen",
152
+ "data-analysis", "cleanup"
153
+ ],
154
+ "actor_types": [
155
+ "principal-investigator", "lab-manager", "senior-scientist",
156
+ "postdoc", "grad-student", "lab-technician", "undergrad"
157
+ ]
158
+ },
159
+ "bakery": {
160
+ "required_tasks": ["oven", "mixer", "work-bench"],
161
+ "common_tasks": [
162
+ "oven", "convection-oven", "proofer", "mixer", "dough-sheeter",
163
+ "work-bench", "prep-work", "decorating-station", "cooling-rack",
164
+ "packaging", "cleanup", "refrigeration", "freezer", "laminator"
165
+ ],
166
+ "actor_types": ["head-baker", "pastry-chef", "baker", "assistant-baker"]
167
+ },
168
+ "airport": {
169
+ "required_tasks": ["runway", "gate"],
170
+ "common_tasks": [
171
+ "runway", "gate", "baggage-handling", "check-in-counter",
172
+ "security-checkpoint", "air-traffic-control", "ground-crew",
173
+ "fueling-station", "maintenance-hangar", "cargo-terminal",
174
+ "customs", "immigration"
175
+ ],
176
+ "actor_types": [
177
+ "pilot", "air-traffic-controller", "ground-crew", "baggage-handler",
178
+ "security-officer", "check-in-agent", "maintenance-technician"
179
+ ]
180
+ }
181
+ }
182
+
183
+ # Additional type aliases
184
+ ENVIRONMENT_TYPE_TASKS.update({
185
+ "home": ENVIRONMENT_TYPE_TASKS["kitchen"],
186
+ "restaurant": ENVIRONMENT_TYPE_TASKS["kitchen"],
187
+ "commercial-kitchen": ENVIRONMENT_TYPE_TASKS["kitchen"],
188
+ "lab": ENVIRONMENT_TYPE_TASKS["laboratory"],
189
+ "research": ENVIRONMENT_TYPE_TASKS["laboratory"],
190
+ "biotech": ENVIRONMENT_TYPE_TASKS["laboratory"],
191
+ "pharma": ENVIRONMENT_TYPE_TASKS["laboratory"],
192
+ "artisan": ENVIRONMENT_TYPE_TASKS["bakery"],
193
+ "pastry": ENVIRONMENT_TYPE_TASKS["bakery"],
194
+ "aviation": ENVIRONMENT_TYPE_TASKS["airport"],
195
+ "runway": ENVIRONMENT_TYPE_TASKS["airport"],
196
+ "terminal": ENVIRONMENT_TYPE_TASKS["airport"]
197
+ })
198
+
199
+
200
+ class EnvironmentValidator:
201
+ """Validator for environment catalog files."""
202
+
203
+ def __init__(self):
204
+ """Initialize the validator with schemas."""
205
+ self.base_schema = BASE_ENVIRONMENT_SCHEMA
206
+ self.type_tasks = ENVIRONMENT_TYPE_TASKS
207
+
208
+ def validate_environment(self, environment_data: Dict[str, Any]) -> List[str]:
209
+ """
210
+ Validate an environment against both base schema and type-specific requirements.
211
+
212
+ Args:
213
+ environment_data: The environment data to validate
214
+
215
+ Returns:
216
+ List of validation errors (empty if valid)
217
+ """
218
+ errors = []
219
+
220
+ # Validate against base schema
221
+ try:
222
+ jsonschema.validate(environment_data, self.base_schema)
223
+ except jsonschema.ValidationError as e:
224
+ errors.append(f"Schema validation error: {e.message}")
225
+ return errors # Don't continue if basic schema is invalid
226
+
227
+ # Get environment type for type-specific validation
228
+ env_type = environment_data.get('type', '')
229
+
230
+ # Validate type-specific requirements
231
+ type_errors = self._validate_type_specific(environment_data, env_type)
232
+ errors.extend(type_errors)
233
+
234
+ # Validate actor types consistency
235
+ actor_errors = self._validate_actor_types(environment_data, env_type)
236
+ errors.extend(actor_errors)
237
+
238
+ # Validate resource constraints
239
+ resource_errors = self._validate_resource_constraints(environment_data)
240
+ errors.extend(resource_errors)
241
+
242
+ return errors
243
+
244
+ def _validate_type_specific(self, environment_data: Dict[str, Any], env_type: str) -> List[str]:
245
+ """Validate type-specific requirements."""
246
+ errors = []
247
+
248
+ if env_type not in self.type_tasks:
249
+ # Don't error for unknown types, just warn
250
+ return []
251
+
252
+ type_config = self.type_tasks[env_type]
253
+ resource_constraints = environment_data.get('resourceConstraints', [])
254
+ task_names = {constraint['task'] for constraint in resource_constraints}
255
+
256
+ # Check for required tasks
257
+ required_tasks = set(type_config.get('required_tasks', []))
258
+ missing_required = required_tasks - task_names
259
+ if missing_required:
260
+ errors.append(f"Missing required tasks for {env_type}: {', '.join(missing_required)}")
261
+
262
+ # Check for unexpected tasks (warn only)
263
+ common_tasks = set(type_config.get('common_tasks', []))
264
+ unexpected_tasks = task_names - common_tasks
265
+ if unexpected_tasks:
266
+ # This is a warning, not an error
267
+ errors.append(f"Warning: Uncommon tasks for {env_type}: {', '.join(unexpected_tasks)}")
268
+
269
+ return errors
270
+
271
+ def _validate_actor_types(self, environment_data: Dict[str, Any], env_type: str) -> List[str]:
272
+ """Validate actor types are appropriate for environment type."""
273
+ errors = []
274
+
275
+ if env_type not in self.type_tasks:
276
+ return []
277
+
278
+ type_config = self.type_tasks[env_type]
279
+ expected_actor_types = set(type_config.get('actor_types', []))
280
+
281
+ # Check actor types (if using new format)
282
+ if 'actorTypes' in environment_data:
283
+ actual_actor_types = set(environment_data['actorTypes'].keys())
284
+
285
+ # Validate actor type names are reasonable for this environment
286
+ unexpected_actors = actual_actor_types - expected_actor_types
287
+ if unexpected_actors:
288
+ errors.append(f"Warning: Uncommon actor types for {env_type}: {', '.join(unexpected_actors)}")
289
+
290
+ return errors
291
+
292
+ def _validate_resource_constraints(self, environment_data: Dict[str, Any]) -> List[str]:
293
+ """Validate resource constraints for consistency."""
294
+ errors = []
295
+
296
+ resource_constraints = environment_data.get('resourceConstraints', [])
297
+ actor_types = environment_data.get('actorTypes', {})
298
+
299
+ # Check that qualified actor types exist
300
+ all_actor_type_ids = set(actor_types.keys())
301
+ if environment_data.get('actors'):
302
+ all_actor_type_ids.add('generic') # Legacy support
303
+
304
+ for constraint in resource_constraints:
305
+ qualified_types = constraint.get('qualifiedActorTypes', [])
306
+ for actor_type in qualified_types:
307
+ if actor_type not in all_actor_type_ids:
308
+ errors.append(f"Task '{constraint['task']}' references unknown actor type: {actor_type}")
309
+
310
+ # Validate actors vs actorsRequired totals don't exceed capacity
311
+ total_actors = 0
312
+ if 'actorTypes' in environment_data:
313
+ total_actors = sum(actor_info['count'] for actor_info in actor_types.values())
314
+ elif 'actors' in environment_data:
315
+ total_actors = environment_data['actors']
316
+
317
+ max_required = 0
318
+ for constraint in resource_constraints:
319
+ actors_required = constraint.get('actorsRequired', 0)
320
+ max_concurrent = constraint.get('maxConcurrent', 1)
321
+ max_required = max(max_required, actors_required * max_concurrent)
322
+
323
+ if max_required > total_actors:
324
+ errors.append(f"Resource constraints require up to {max_required} actors but only {total_actors} available")
325
+
326
+ return errors
327
+
328
+ def validate_environment_file(self, file_path: str) -> List[str]:
329
+ """
330
+ Validate an environment file.
331
+
332
+ Args:
333
+ file_path: Path to the environment file
334
+
335
+ Returns:
336
+ List of validation errors
337
+ """
338
+ try:
339
+ with open(file_path, 'r') as f:
340
+ if file_path.endswith('.json'):
341
+ data = json.load(f)
342
+ else:
343
+ import yaml
344
+ data = yaml.safe_load(f)
345
+
346
+ return self.validate_environment(data)
347
+
348
+ except FileNotFoundError:
349
+ return [f"File not found: {file_path}"]
350
+ except json.JSONDecodeError as e:
351
+ return [f"JSON decode error: {e}"]
352
+ except Exception as e:
353
+ return [f"Error reading file: {e}"]
354
+
355
+ def get_environment_type_info(self, env_type: str) -> Optional[Dict[str, Any]]:
356
+ """Get information about a specific environment type."""
357
+ return self.type_tasks.get(env_type)
358
+
359
+ def list_supported_types(self) -> List[str]:
360
+ """Get list of supported environment types."""
361
+ return list(self.type_tasks.keys())
362
+
363
+ def suggest_tasks_for_type(self, env_type: str) -> List[str]:
364
+ """Suggest common tasks for an environment type."""
365
+ if env_type in self.type_tasks:
366
+ return self.type_tasks[env_type].get('common_tasks', [])
367
+ return []
368
+
369
+ def suggest_actor_types_for_type(self, env_type: str) -> List[str]:
370
+ """Suggest actor types for an environment type."""
371
+ if env_type in self.type_tasks:
372
+ return self.type_tasks[env_type].get('actor_types', [])
373
+ return []
374
+
375
+
376
+ def validate_all_environments(environments_dir: str = "environments") -> Dict[str, List[str]]:
377
+ """
378
+ Validate all environment files in a directory.
379
+
380
+ Args:
381
+ environments_dir: Directory containing environment files
382
+
383
+ Returns:
384
+ Dictionary mapping file names to validation errors
385
+ """
386
+ validator = EnvironmentValidator()
387
+ results = {}
388
+
389
+ env_path = Path(environments_dir)
390
+ if not env_path.exists():
391
+ return {"error": ["Environments directory not found"]}
392
+
393
+ for file_path in env_path.glob("*.json"):
394
+ errors = validator.validate_environment_file(str(file_path))
395
+ if errors:
396
+ results[file_path.name] = errors
397
+
398
+ for file_path in env_path.glob("*.yaml"):
399
+ errors = validator.validate_environment_file(str(file_path))
400
+ if errors:
401
+ results[file_path.name] = errors
402
+
403
+ for file_path in env_path.glob("*.yml"):
404
+ errors = validator.validate_environment_file(str(file_path))
405
+ if errors:
406
+ results[file_path.name] = errors
407
+
408
+ return results
409
+
410
+
411
+ if __name__ == "__main__":
412
+ # Example usage and testing
413
+ validator = EnvironmentValidator()
414
+
415
+ print("Environment Schema Validation System")
416
+ print("=" * 50)
417
+
418
+ print(f"\nSupported environment types: {len(validator.list_supported_types())}")
419
+ for env_type in sorted(validator.list_supported_types()):
420
+ info = validator.get_environment_type_info(env_type)
421
+ required = info.get('required_tasks', [])
422
+ print(f" {env_type:20} - Required: {', '.join(required) if required else 'None'}")
423
+
424
+ # Test validation on existing environment files
425
+ print(f"\nValidating existing environment files...")
426
+ validation_results = validate_all_environments()
427
+
428
+ if not validation_results:
429
+ print("✓ All environment files are valid!")
430
+ else:
431
+ for filename, errors in validation_results.items():
432
+ print(f"\n{filename}:")
433
+ for error in errors:
434
+ print(f" - {error}")
435
+
436
+ # Test the specific environment types requested
437
+ print(f"\nEnvironment type suggestions:")
438
+ for env_type in ["kitchen", "laboratory", "bakery", "airport"]:
439
+ tasks = validator.suggest_tasks_for_type(env_type)
440
+ actors = validator.suggest_actor_types_for_type(env_type)
441
+ print(f"\n{env_type}:")
442
+ print(f" Suggested tasks: {', '.join(tasks[:5])}...")
443
+ print(f" Suggested actors: {', '.join(actors)}")