grai-build 0.3.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.
@@ -0,0 +1,375 @@
1
+ """
2
+ YAML parser for grai.build.
3
+
4
+ This module provides functions to parse YAML files containing entity and relation
5
+ definitions and convert them into Pydantic models.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ import yaml
12
+ from pydantic import ValidationError
13
+
14
+ from grai.core.models import Entity, Project, Property, Relation, RelationMapping
15
+
16
+
17
+ class ParserError(Exception):
18
+ """Base exception for parser errors."""
19
+
20
+ def __init__(self, message: str, file_path: Optional[Path] = None):
21
+ """
22
+ Initialize parser error.
23
+
24
+ Args:
25
+ message: Error message.
26
+ file_path: Optional path to the file that caused the error.
27
+ """
28
+ self.file_path = file_path
29
+ if file_path:
30
+ super().__init__(f"{file_path}: {message}")
31
+ else:
32
+ super().__init__(message)
33
+
34
+
35
+ class YAMLParseError(ParserError):
36
+ """Exception raised when YAML parsing fails."""
37
+
38
+ pass
39
+
40
+
41
+ class ValidationParserError(ParserError):
42
+ """Exception raised when Pydantic validation fails."""
43
+
44
+ pass
45
+
46
+
47
+ def load_yaml_file(file_path: Path) -> Dict[str, Any]:
48
+ """
49
+ Load and parse a YAML file.
50
+
51
+ Args:
52
+ file_path: Path to the YAML file.
53
+
54
+ Returns:
55
+ Parsed YAML content as a dictionary.
56
+
57
+ Raises:
58
+ YAMLParseError: If the file cannot be read or parsed.
59
+ """
60
+ try:
61
+ with open(file_path, "r", encoding="utf-8") as f:
62
+ content = yaml.safe_load(f)
63
+ if content is None:
64
+ raise YAMLParseError("File is empty or contains only comments", file_path)
65
+ if not isinstance(content, dict):
66
+ raise YAMLParseError(
67
+ f"Expected YAML object (dict), got {type(content).__name__}", file_path
68
+ )
69
+ return content
70
+ except FileNotFoundError:
71
+ raise YAMLParseError(f"File not found: {file_path}", file_path)
72
+ except yaml.YAMLError as e:
73
+ raise YAMLParseError(f"Invalid YAML syntax: {e}", file_path)
74
+ except Exception as e:
75
+ raise YAMLParseError(f"Failed to read file: {e}", file_path)
76
+
77
+
78
+ def parse_property(data: Dict[str, Any]) -> Property:
79
+ """
80
+ Parse a property definition from a dictionary.
81
+
82
+ Args:
83
+ data: Dictionary containing property data.
84
+
85
+ Returns:
86
+ Property instance.
87
+
88
+ Raises:
89
+ ValidationParserError: If validation fails.
90
+ """
91
+ try:
92
+ return Property(**data)
93
+ except ValidationError as e:
94
+ raise ValidationParserError(f"Invalid property definition: {e}")
95
+
96
+
97
+ def parse_entity(data: Dict[str, Any], file_path: Optional[Path] = None) -> Entity:
98
+ """
99
+ Parse an entity definition from a dictionary.
100
+
101
+ Args:
102
+ data: Dictionary containing entity data.
103
+ file_path: Optional path to the source file for error messages.
104
+
105
+ Returns:
106
+ Entity instance.
107
+
108
+ Raises:
109
+ ValidationParserError: If validation fails.
110
+ """
111
+ try:
112
+ # Parse properties if they exist
113
+ if "properties" in data and isinstance(data["properties"], list):
114
+ data["properties"] = [parse_property(prop) for prop in data["properties"]]
115
+
116
+ return Entity(**data)
117
+ except ValidationError as e:
118
+ raise ValidationParserError(f"Invalid entity definition: {e}", file_path)
119
+ except ValidationParserError:
120
+ raise
121
+ except Exception as e:
122
+ raise ValidationParserError(f"Failed to parse entity: {e}", file_path)
123
+
124
+
125
+ def parse_relation(data: Dict[str, Any], file_path: Optional[Path] = None) -> Relation:
126
+ """
127
+ Parse a relation definition from a dictionary.
128
+
129
+ Args:
130
+ data: Dictionary containing relation data.
131
+ file_path: Optional path to the source file for error messages.
132
+
133
+ Returns:
134
+ Relation instance.
135
+
136
+ Raises:
137
+ ValidationParserError: If validation fails.
138
+ """
139
+ try:
140
+ # Parse properties if they exist
141
+ if "properties" in data and isinstance(data["properties"], list):
142
+ data["properties"] = [parse_property(prop) for prop in data["properties"]]
143
+
144
+ # Parse mappings if they exist
145
+ if "mappings" in data and isinstance(data["mappings"], dict):
146
+ data["mappings"] = RelationMapping(**data["mappings"])
147
+
148
+ return Relation(**data)
149
+ except ValidationError as e:
150
+ raise ValidationParserError(f"Invalid relation definition: {e}", file_path)
151
+ except ValidationParserError:
152
+ raise
153
+ except Exception as e:
154
+ raise ValidationParserError(f"Failed to parse relation: {e}", file_path)
155
+
156
+
157
+ def parse_entity_file(file_path: Union[str, Path]) -> Entity:
158
+ """
159
+ Parse an entity definition from a YAML file.
160
+
161
+ Args:
162
+ file_path: Path to the entity YAML file.
163
+
164
+ Returns:
165
+ Entity instance.
166
+
167
+ Raises:
168
+ ParserError: If parsing fails.
169
+ """
170
+ path = Path(file_path)
171
+ data = load_yaml_file(path)
172
+ return parse_entity(data, path)
173
+
174
+
175
+ def parse_relation_file(file_path: Union[str, Path]) -> Relation:
176
+ """
177
+ Parse a relation definition from a YAML file.
178
+
179
+ Args:
180
+ file_path: Path to the relation YAML file.
181
+
182
+ Returns:
183
+ Relation instance.
184
+
185
+ Raises:
186
+ ParserError: If parsing fails.
187
+ """
188
+ path = Path(file_path)
189
+ data = load_yaml_file(path)
190
+ return parse_relation(data, path)
191
+
192
+
193
+ def discover_yaml_files(directory: Path, pattern: str = "*.yml") -> List[Path]:
194
+ """
195
+ Recursively discover YAML files in a directory.
196
+
197
+ Args:
198
+ directory: Directory to search.
199
+ pattern: Glob pattern for file matching (default: "*.yml").
200
+
201
+ Returns:
202
+ List of paths to YAML files.
203
+ """
204
+ if not directory.exists():
205
+ return []
206
+
207
+ if not directory.is_dir():
208
+ return []
209
+
210
+ # Use rglob for recursive search
211
+ yaml_files = list(directory.glob(pattern))
212
+ yaml_files.extend(directory.glob(pattern.replace(".yml", ".yaml")))
213
+
214
+ return sorted(yaml_files)
215
+
216
+
217
+ def load_entities_from_directory(directory: Union[str, Path]) -> List[Entity]:
218
+ """
219
+ Load all entity definitions from a directory.
220
+
221
+ Args:
222
+ directory: Path to directory containing entity YAML files.
223
+
224
+ Returns:
225
+ List of Entity instances.
226
+
227
+ Raises:
228
+ ParserError: If parsing any file fails.
229
+ """
230
+ path = Path(directory)
231
+ if not path.exists():
232
+ raise ParserError(f"Directory not found: {path}")
233
+
234
+ yaml_files = discover_yaml_files(path)
235
+ entities = []
236
+ errors = []
237
+
238
+ for file_path in yaml_files:
239
+ try:
240
+ entity = parse_entity_file(file_path)
241
+ entities.append(entity)
242
+ except ParserError as e:
243
+ errors.append(str(e))
244
+
245
+ if errors:
246
+ error_msg = "\n".join(errors)
247
+ raise ParserError(f"Failed to load entities:\n{error_msg}")
248
+
249
+ return entities
250
+
251
+
252
+ def load_relations_from_directory(directory: Union[str, Path]) -> List[Relation]:
253
+ """
254
+ Load all relation definitions from a directory.
255
+
256
+ Args:
257
+ directory: Path to directory containing relation YAML files.
258
+
259
+ Returns:
260
+ List of Relation instances.
261
+
262
+ Raises:
263
+ ParserError: If parsing any file fails.
264
+ """
265
+ path = Path(directory)
266
+ if not path.exists():
267
+ raise ParserError(f"Directory not found: {path}")
268
+
269
+ yaml_files = discover_yaml_files(path)
270
+ relations = []
271
+ errors = []
272
+
273
+ for file_path in yaml_files:
274
+ try:
275
+ relation = parse_relation_file(file_path)
276
+ relations.append(relation)
277
+ except ParserError as e:
278
+ errors.append(str(e))
279
+
280
+ if errors:
281
+ error_msg = "\n".join(errors)
282
+ raise ParserError(f"Failed to load relations:\n{error_msg}")
283
+
284
+ return relations
285
+
286
+
287
+ def load_project_manifest(file_path: Union[str, Path] = "grai.yml") -> Dict[str, Any]:
288
+ """
289
+ Load the project manifest (grai.yml).
290
+
291
+ Args:
292
+ file_path: Path to the grai.yml file (default: "grai.yml").
293
+
294
+ Returns:
295
+ Dictionary containing project configuration.
296
+
297
+ Raises:
298
+ ParserError: If the file cannot be loaded.
299
+ """
300
+ path = Path(file_path)
301
+ return load_yaml_file(path)
302
+
303
+
304
+ def load_project(
305
+ project_root: Union[str, Path],
306
+ entities_dir: str = "entities",
307
+ relations_dir: str = "relations",
308
+ manifest_file: str = "grai.yml",
309
+ ) -> Project:
310
+ """
311
+ Load a complete grai.build project from a directory structure.
312
+
313
+ Expected structure:
314
+ project_root/
315
+ ├── grai.yml
316
+ ├── entities/
317
+ │ ├── entity1.yml
318
+ │ └── entity2.yml
319
+ └── relations/
320
+ └── relation1.yml
321
+
322
+ Args:
323
+ project_root: Root directory of the project.
324
+ entities_dir: Subdirectory containing entity definitions (default: "entities").
325
+ relations_dir: Subdirectory containing relation definitions (default: "relations").
326
+ manifest_file: Name of the project manifest file (default: "grai.yml").
327
+
328
+ Returns:
329
+ Project instance with all entities and relations loaded.
330
+
331
+ Raises:
332
+ ParserError: If loading fails.
333
+ """
334
+ root = Path(project_root)
335
+
336
+ if not root.exists():
337
+ raise ParserError(f"Project root not found: {root}")
338
+
339
+ # Load manifest
340
+ manifest_path = root / manifest_file
341
+ try:
342
+ manifest = load_project_manifest(manifest_path)
343
+ except ParserError as e:
344
+ raise ParserError(f"Failed to load project manifest: {e}")
345
+
346
+ # Load entities
347
+ entities_path = root / entities_dir
348
+ entities = []
349
+ if entities_path.exists():
350
+ try:
351
+ entities = load_entities_from_directory(entities_path)
352
+ except ParserError as e:
353
+ raise ParserError(f"Failed to load entities: {e}")
354
+
355
+ # Load relations
356
+ relations_path = root / relations_dir
357
+ relations = []
358
+ if relations_path.exists():
359
+ try:
360
+ relations = load_relations_from_directory(relations_path)
361
+ except ParserError as e:
362
+ raise ParserError(f"Failed to load relations: {e}")
363
+
364
+ # Create project
365
+ try:
366
+ project = Project(
367
+ name=manifest.get("name", "unnamed-project"),
368
+ version=manifest.get("version", "1.0.0"),
369
+ entities=entities,
370
+ relations=relations,
371
+ config=manifest.get("config", {}),
372
+ )
373
+ return project
374
+ except ValidationError as e:
375
+ raise ValidationParserError(f"Invalid project configuration: {e}")
@@ -0,0 +1,25 @@
1
+ """Validator module for checking project consistency and correctness."""
2
+
3
+ from grai.core.validator.validator import (
4
+ EntityReferenceError,
5
+ KeyMappingError,
6
+ ValidationError,
7
+ ValidationResult,
8
+ validate_entity,
9
+ validate_entity_references,
10
+ validate_key_mappings,
11
+ validate_project,
12
+ validate_relation,
13
+ )
14
+
15
+ __all__ = [
16
+ "ValidationError",
17
+ "EntityReferenceError",
18
+ "KeyMappingError",
19
+ "ValidationResult",
20
+ "validate_project",
21
+ "validate_entity",
22
+ "validate_relation",
23
+ "validate_entity_references",
24
+ "validate_key_mappings",
25
+ ]