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/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
+ ]