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.
- grai/__init__.py +11 -0
- grai/cli/__init__.py +5 -0
- grai/cli/main.py +2546 -0
- grai/core/__init__.py +1 -0
- grai/core/cache/__init__.py +33 -0
- grai/core/cache/build_cache.py +352 -0
- grai/core/compiler/__init__.py +23 -0
- grai/core/compiler/cypher_compiler.py +426 -0
- grai/core/exporter/__init__.py +13 -0
- grai/core/exporter/ir_exporter.py +343 -0
- grai/core/lineage/__init__.py +42 -0
- grai/core/lineage/lineage_tracker.py +685 -0
- grai/core/loader/__init__.py +21 -0
- grai/core/loader/neo4j_loader.py +514 -0
- grai/core/models.py +344 -0
- grai/core/parser/__init__.py +25 -0
- grai/core/parser/yaml_parser.py +375 -0
- grai/core/validator/__init__.py +25 -0
- grai/core/validator/validator.py +475 -0
- grai/core/visualizer/__init__.py +650 -0
- grai/core/visualizer/visualizer.py +15 -0
- grai/templates/__init__.py +1 -0
- grai_build-0.3.0.dist-info/METADATA +374 -0
- grai_build-0.3.0.dist-info/RECORD +28 -0
- grai_build-0.3.0.dist-info/WHEEL +5 -0
- grai_build-0.3.0.dist-info/entry_points.txt +2 -0
- grai_build-0.3.0.dist-info/licenses/LICENSE +21 -0
- grai_build-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|