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
grai/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core modules for grai.build - models, parser, validator, compiler, and loader."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache module for incremental builds.
|
|
3
|
+
|
|
4
|
+
Exports cache management functions for tracking file changes and build artifacts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .build_cache import (
|
|
8
|
+
BuildCache,
|
|
9
|
+
CacheEntry,
|
|
10
|
+
clear_cache,
|
|
11
|
+
compute_file_hash,
|
|
12
|
+
get_cache_path,
|
|
13
|
+
get_changed_files,
|
|
14
|
+
is_file_modified,
|
|
15
|
+
load_cache,
|
|
16
|
+
save_cache,
|
|
17
|
+
should_rebuild,
|
|
18
|
+
update_cache,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BuildCache",
|
|
23
|
+
"CacheEntry",
|
|
24
|
+
"compute_file_hash",
|
|
25
|
+
"should_rebuild",
|
|
26
|
+
"get_cache_path",
|
|
27
|
+
"load_cache",
|
|
28
|
+
"save_cache",
|
|
29
|
+
"clear_cache",
|
|
30
|
+
"get_changed_files",
|
|
31
|
+
"is_file_modified",
|
|
32
|
+
"update_cache",
|
|
33
|
+
]
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build cache for incremental compilation.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to track file changes and enable fast incremental builds
|
|
5
|
+
by computing file hashes and comparing them against cached values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Optional, Set
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CacheEntry:
|
|
18
|
+
"""
|
|
19
|
+
Represents a cached file entry with metadata.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
path: Relative path to the file
|
|
23
|
+
hash: SHA256 hash of file contents
|
|
24
|
+
last_modified: Timestamp of last modification
|
|
25
|
+
size: File size in bytes
|
|
26
|
+
dependencies: List of files this file depends on
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
path: str
|
|
30
|
+
hash: str
|
|
31
|
+
last_modified: str
|
|
32
|
+
size: int
|
|
33
|
+
dependencies: List[str] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
"""Convert to dictionary for JSON serialization."""
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: dict) -> "CacheEntry":
|
|
41
|
+
"""Create CacheEntry from dictionary."""
|
|
42
|
+
return cls(**data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class BuildCache:
|
|
47
|
+
"""
|
|
48
|
+
Build cache containing file hashes and metadata.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
version: Cache format version
|
|
52
|
+
created_at: Cache creation timestamp
|
|
53
|
+
last_updated: Last update timestamp
|
|
54
|
+
entries: Dictionary mapping file paths to cache entries
|
|
55
|
+
project_name: Name of the project
|
|
56
|
+
project_version: Version of the project
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
version: str = "1.0.0"
|
|
60
|
+
created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
61
|
+
last_updated: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
62
|
+
entries: Dict[str, CacheEntry] = field(default_factory=dict)
|
|
63
|
+
project_name: Optional[str] = None
|
|
64
|
+
project_version: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict:
|
|
67
|
+
"""Convert to dictionary for JSON serialization."""
|
|
68
|
+
return {
|
|
69
|
+
"version": self.version,
|
|
70
|
+
"created_at": self.created_at,
|
|
71
|
+
"last_updated": self.last_updated,
|
|
72
|
+
"entries": {path: entry.to_dict() for path, entry in self.entries.items()},
|
|
73
|
+
"project_name": self.project_name,
|
|
74
|
+
"project_version": self.project_version,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_dict(cls, data: dict) -> "BuildCache":
|
|
79
|
+
"""Create BuildCache from dictionary."""
|
|
80
|
+
entries = {
|
|
81
|
+
path: CacheEntry.from_dict(entry_data)
|
|
82
|
+
for path, entry_data in data.get("entries", {}).items()
|
|
83
|
+
}
|
|
84
|
+
return cls(
|
|
85
|
+
version=data.get("version", "1.0.0"),
|
|
86
|
+
created_at=data.get("created_at", datetime.now(UTC).isoformat()),
|
|
87
|
+
last_updated=data.get("last_updated", datetime.now(UTC).isoformat()),
|
|
88
|
+
entries=entries,
|
|
89
|
+
project_name=data.get("project_name"),
|
|
90
|
+
project_version=data.get("project_version"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def compute_file_hash(file_path: Path) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Compute SHA256 hash of a file.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
file_path: Path to the file
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Hexadecimal SHA256 hash string
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
FileNotFoundError: If file doesn't exist
|
|
106
|
+
"""
|
|
107
|
+
if not file_path.exists():
|
|
108
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
109
|
+
|
|
110
|
+
sha256 = hashlib.sha256()
|
|
111
|
+
with open(file_path, "rb") as f:
|
|
112
|
+
# Read in chunks for memory efficiency
|
|
113
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
114
|
+
sha256.update(chunk)
|
|
115
|
+
|
|
116
|
+
return sha256.hexdigest()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_cache_path(project_dir: Path) -> Path:
|
|
120
|
+
"""
|
|
121
|
+
Get the path to the cache file for a project.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
project_dir: Project directory
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Path to .grai/cache.json
|
|
128
|
+
"""
|
|
129
|
+
return project_dir / ".grai" / "cache.json"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_cache(project_dir: Path) -> Optional[BuildCache]:
|
|
133
|
+
"""
|
|
134
|
+
Load build cache from disk.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
project_dir: Project directory
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
BuildCache if cache exists and is valid, None otherwise
|
|
141
|
+
"""
|
|
142
|
+
cache_path = get_cache_path(project_dir)
|
|
143
|
+
|
|
144
|
+
if not cache_path.exists():
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
with open(cache_path, "r") as f:
|
|
149
|
+
data = json.load(f)
|
|
150
|
+
return BuildCache.from_dict(data)
|
|
151
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
152
|
+
# Invalid cache, return None
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def save_cache(cache: BuildCache, project_dir: Path) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Save build cache to disk.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
cache: BuildCache to save
|
|
162
|
+
project_dir: Project directory
|
|
163
|
+
"""
|
|
164
|
+
cache_path = get_cache_path(project_dir)
|
|
165
|
+
|
|
166
|
+
# Create .grai directory if it doesn't exist
|
|
167
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
|
|
169
|
+
# Update last_updated timestamp
|
|
170
|
+
cache.last_updated = datetime.now(UTC).isoformat()
|
|
171
|
+
|
|
172
|
+
# Write cache
|
|
173
|
+
with open(cache_path, "w") as f:
|
|
174
|
+
json.dump(cache.to_dict(), f, indent=2)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def clear_cache(project_dir: Path) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Clear the build cache.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
project_dir: Project directory
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if cache was deleted, False if no cache existed
|
|
186
|
+
"""
|
|
187
|
+
cache_path = get_cache_path(project_dir)
|
|
188
|
+
|
|
189
|
+
if cache_path.exists():
|
|
190
|
+
cache_path.unlink()
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def is_file_modified(file_path: Path, cache_entry: Optional[CacheEntry]) -> bool:
|
|
197
|
+
"""
|
|
198
|
+
Check if a file has been modified since last cache.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
file_path: Path to the file
|
|
202
|
+
cache_entry: Cache entry for the file (None if not cached)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if file is new or modified, False if unchanged
|
|
206
|
+
"""
|
|
207
|
+
# If no cache entry, file is new
|
|
208
|
+
if cache_entry is None:
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
# If file doesn't exist, it was deleted
|
|
212
|
+
if not file_path.exists():
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
# Check if size changed (fast check)
|
|
216
|
+
stat = file_path.stat()
|
|
217
|
+
if stat.st_size != cache_entry.size:
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
# Compute and compare hash
|
|
221
|
+
current_hash = compute_file_hash(file_path)
|
|
222
|
+
return current_hash != cache_entry.hash
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_changed_files(project_dir: Path, cache: Optional[BuildCache]) -> Dict[str, Set[Path]]:
|
|
226
|
+
"""
|
|
227
|
+
Get all files that have changed since last build.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
project_dir: Project directory
|
|
231
|
+
cache: Build cache (None for first build)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Dictionary with keys: 'added', 'modified', 'deleted' mapping to sets of file paths
|
|
235
|
+
"""
|
|
236
|
+
changes: Dict[str, Set[Path]] = {
|
|
237
|
+
"added": set(),
|
|
238
|
+
"modified": set(),
|
|
239
|
+
"deleted": set(),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# If no cache, all files are new
|
|
243
|
+
if cache is None:
|
|
244
|
+
# Find all YAML files
|
|
245
|
+
for pattern in ["entities/*.yml", "relations/*.yml", "grai.yml"]:
|
|
246
|
+
for file_path in project_dir.glob(pattern):
|
|
247
|
+
if file_path.is_file():
|
|
248
|
+
changes["added"].add(file_path)
|
|
249
|
+
return changes
|
|
250
|
+
|
|
251
|
+
# Track seen files
|
|
252
|
+
seen_files: Set[str] = set()
|
|
253
|
+
|
|
254
|
+
# Check all current files
|
|
255
|
+
for pattern in ["entities/*.yml", "relations/*.yml", "grai.yml"]:
|
|
256
|
+
for file_path in project_dir.glob(pattern):
|
|
257
|
+
if not file_path.is_file():
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
rel_path = str(file_path.relative_to(project_dir))
|
|
261
|
+
seen_files.add(rel_path)
|
|
262
|
+
|
|
263
|
+
cache_entry = cache.entries.get(rel_path)
|
|
264
|
+
|
|
265
|
+
if cache_entry is None:
|
|
266
|
+
changes["added"].add(file_path)
|
|
267
|
+
elif is_file_modified(file_path, cache_entry):
|
|
268
|
+
changes["modified"].add(file_path)
|
|
269
|
+
|
|
270
|
+
# Check for deleted files
|
|
271
|
+
for cached_path in cache.entries.keys():
|
|
272
|
+
if cached_path not in seen_files:
|
|
273
|
+
changes["deleted"].add(project_dir / cached_path)
|
|
274
|
+
|
|
275
|
+
return changes
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def should_rebuild(
|
|
279
|
+
project_dir: Path, cache: Optional[BuildCache] = None
|
|
280
|
+
) -> tuple[bool, Dict[str, Set[Path]]]:
|
|
281
|
+
"""
|
|
282
|
+
Determine if project needs to be rebuilt.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
project_dir: Project directory
|
|
286
|
+
cache: Build cache (will load from disk if None)
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Tuple of (should_rebuild: bool, changes: Dict[str, Set[Path]])
|
|
290
|
+
"""
|
|
291
|
+
# Load cache if not provided
|
|
292
|
+
if cache is None:
|
|
293
|
+
cache = load_cache(project_dir)
|
|
294
|
+
|
|
295
|
+
# Get changed files
|
|
296
|
+
changes = get_changed_files(project_dir, cache)
|
|
297
|
+
|
|
298
|
+
# Rebuild if any changes
|
|
299
|
+
has_changes = any(len(files) > 0 for files in changes.values())
|
|
300
|
+
|
|
301
|
+
return has_changes, changes
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def update_cache(
|
|
305
|
+
project_dir: Path, project_name: Optional[str] = None, project_version: Optional[str] = None
|
|
306
|
+
) -> BuildCache:
|
|
307
|
+
"""
|
|
308
|
+
Update cache with current file hashes.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
project_dir: Project directory
|
|
312
|
+
project_name: Project name (optional)
|
|
313
|
+
project_version: Project version (optional)
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Updated BuildCache
|
|
317
|
+
"""
|
|
318
|
+
# Load existing cache or create new one
|
|
319
|
+
cache = load_cache(project_dir) or BuildCache()
|
|
320
|
+
|
|
321
|
+
# Update project info if provided
|
|
322
|
+
if project_name:
|
|
323
|
+
cache.project_name = project_name
|
|
324
|
+
if project_version:
|
|
325
|
+
cache.project_version = project_version
|
|
326
|
+
|
|
327
|
+
# Clear old entries
|
|
328
|
+
cache.entries.clear()
|
|
329
|
+
|
|
330
|
+
# Add all current files
|
|
331
|
+
for pattern in ["entities/*.yml", "relations/*.yml", "grai.yml"]:
|
|
332
|
+
for file_path in project_dir.glob(pattern):
|
|
333
|
+
if not file_path.is_file():
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
rel_path = str(file_path.relative_to(project_dir))
|
|
337
|
+
stat = file_path.stat()
|
|
338
|
+
|
|
339
|
+
# Create cache entry
|
|
340
|
+
entry = CacheEntry(
|
|
341
|
+
path=rel_path,
|
|
342
|
+
hash=compute_file_hash(file_path),
|
|
343
|
+
last_modified=datetime.fromtimestamp(stat.st_mtime, UTC).isoformat(),
|
|
344
|
+
size=stat.st_size,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
cache.entries[rel_path] = entry
|
|
348
|
+
|
|
349
|
+
# Save cache
|
|
350
|
+
save_cache(cache, project_dir)
|
|
351
|
+
|
|
352
|
+
return cache
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Compiler module for generating database queries from models."""
|
|
2
|
+
|
|
3
|
+
from grai.core.compiler.cypher_compiler import (
|
|
4
|
+
CompilerError,
|
|
5
|
+
compile_and_write,
|
|
6
|
+
compile_entity,
|
|
7
|
+
compile_project,
|
|
8
|
+
compile_relation,
|
|
9
|
+
compile_schema_only,
|
|
10
|
+
generate_load_csv_statements,
|
|
11
|
+
write_cypher_file,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CompilerError",
|
|
16
|
+
"compile_entity",
|
|
17
|
+
"compile_relation",
|
|
18
|
+
"compile_project",
|
|
19
|
+
"write_cypher_file",
|
|
20
|
+
"compile_and_write",
|
|
21
|
+
"generate_load_csv_statements",
|
|
22
|
+
"compile_schema_only",
|
|
23
|
+
]
|