nadata 0.1.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.
- nadata/__init__.py +15 -0
- nadata/api.py +894 -0
- nadata/cli.py +445 -0
- nadata/console.py +486 -0
- nadata/core/__init__.py +0 -0
- nadata/core/database/__init__.py +38 -0
- nadata/core/database/builtins.py +87 -0
- nadata/core/database/database.py +958 -0
- nadata/core/database/dumper.py +285 -0
- nadata/core/database/entity_id.py +142 -0
- nadata/core/database/instance.py +706 -0
- nadata/core/database/network_index.py +1142 -0
- nadata/core/database/page_manager.py +175 -0
- nadata/core/database/persistence/__init__.py +28 -0
- nadata/core/database/persistence/binary_cells.py +1141 -0
- nadata/core/database/persistence/compaction.py +97 -0
- nadata/core/database/persistence/dumper.py +798 -0
- nadata/core/database/persistence/lazy_instances.py +1537 -0
- nadata/core/database/persistence/loader.py +424 -0
- nadata/core/database/persistence/record_store.py +228 -0
- nadata/core/database/persistence/serialization.py +605 -0
- nadata/core/database/query_engine.py +222 -0
- nadata/core/database/queryset.py +508 -0
- nadata/core/database/reconciliation.py +287 -0
- nadata/core/database/slotted_page.py +190 -0
- nadata/core/database/spatial_index.py +1080 -0
- nadata/core/expressions/__init__.py +44 -0
- nadata/core/expressions/autograd.py +2909 -0
- nadata/core/expressions/builtins.py +4673 -0
- nadata/core/expressions/expression.py +2658 -0
- nadata/core/expressions/io_builtins.py +972 -0
- nadata/core/expressions/scope.py +336 -0
- nadata/core/expressions/statement.py +953 -0
- nadata/core/struct/__init__.py +14 -0
- nadata/core/struct/struct.py +218 -0
- nadata/core/struct/struct_attribute.py +100 -0
- nadata/core/struct/struct_method.py +324 -0
- nadata/core/types/__init__.py +33 -0
- nadata/core/types/bits/__init__.py +0 -0
- nadata/core/types/bits/bits.py +686 -0
- nadata/core/types/home_types/__init__.py +19 -0
- nadata/core/types/home_types/color.py +102 -0
- nadata/core/types/home_types/coordinate.py +1008 -0
- nadata/core/types/home_types/datetime.py +362 -0
- nadata/core/types/home_types/geogrid.py +92 -0
- nadata/core/types/home_types/geometry.py +1223 -0
- nadata/core/types/home_types/graph.py +0 -0
- nadata/core/types/home_types/planet.py +359 -0
- nadata/core/types/home_types/sea_level.py +98 -0
- nadata/core/types/home_types/vec.py +0 -0
- nadata/core/types/types.py +1345 -0
- nadata/core/values/__init__.py +14 -0
- nadata/core/values/values.py +1568 -0
- nadata/parser/__init__.py +0 -0
- nadata/parser/expressions/__init__.py +6 -0
- nadata/parser/expressions/dependencies.py +318 -0
- nadata/parser/expressions/expression_parser.py +993 -0
- nadata/parser/expressions/splitter.py +98 -0
- nadata/parser/expressions/statement_parser.py +1425 -0
- nadata/parser/expressions/tokenizer.py +402 -0
- nadata/parser/expressions/type_parser.py +187 -0
- nadata/parser/nadata_loader/__init__.py +8 -0
- nadata/parser/nadata_loader/loader.py +951 -0
- nadata/parser/nastruct_parser/__init__.py +0 -0
- nadata/parser/nastruct_parser/declaration.py +105 -0
- nadata/parser/nastruct_parser/imports.py +164 -0
- nadata/parser/nastruct_parser/nastruct_parser.py +209 -0
- nadata/parser/nastruct_parser/parse_attributes.py +149 -0
- nadata/parser/nastruct_parser/parse_callables.py +436 -0
- nadata/parser/nastruct_parser/parse_expression.py +59 -0
- nadata/parser/nastruct_parser/parse_structs.py +206 -0
- nadata/parser/nastruct_parser/parser_state.py +84 -0
- nadata/parser/nastruct_parser/resolver.py +1412 -0
- nadata/parser/nastruct_parser/struct_body_state.py +50 -0
- nadata/parser/nastruct_parser/tmp_structs.py +156 -0
- nadata/parser/utils/__init__.py +20 -0
- nadata/parser/utils/brackets.py +252 -0
- nadata/parser/utils/comments_remove.py +149 -0
- nadata/parser/utils/identifiers.py +121 -0
- nadata/session.py +868 -0
- nadata-0.1.0.dist-info/METADATA +191 -0
- nadata-0.1.0.dist-info/RECORD +86 -0
- nadata-0.1.0.dist-info/WHEEL +5 -0
- nadata-0.1.0.dist-info/entry_points.txt +2 -0
- nadata-0.1.0.dist-info/licenses/LICENSE +21 -0
- nadata-0.1.0.dist-info/top_level.txt +1 -0
nadata/console.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive console mode for Nadata.
|
|
3
|
+
|
|
4
|
+
After loading a project folder or a persisted .ndb database, the console
|
|
5
|
+
evaluates one statement batch per request: source lines accumulate until a
|
|
6
|
+
blank line (double ENTER), then execute as a whole — bare expressions echo
|
|
7
|
+
their value, declarations and scripts (including multi-line loop bodies)
|
|
8
|
+
execute against the live database, and all state (instances, symbols, and
|
|
9
|
+
script variables) persists across requests.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Import Modules
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from nadata.api import Nadata
|
|
17
|
+
from nadata.core.expressions.expression import Expression
|
|
18
|
+
from nadata.core.expressions.statement import ExprStmt, Statement
|
|
19
|
+
from nadata.core.expressions.scope import LoopLimitError
|
|
20
|
+
from nadata.core.database.persistence.dumper import flush_dirty_instances
|
|
21
|
+
from nadata.core.values.values import Value, ValueExternal
|
|
22
|
+
from nadata.parser.nadata_loader.loader import NaDataLoader, format_runtime_value
|
|
23
|
+
from nadata.session import ScriptFile, parse_script_file, scan_folder
|
|
24
|
+
|
|
25
|
+
# Commands handled by the console itself, not the language
|
|
26
|
+
CONSOLE_COMMANDS: dict[str, str] = {
|
|
27
|
+
":help": "Show this help.",
|
|
28
|
+
":info": "Print a summary of the loaded database.",
|
|
29
|
+
":classes": "List the registered structs.",
|
|
30
|
+
":symbols": "List the bound data symbols.",
|
|
31
|
+
":run": "Run a #manual script: :run file.nadata [name=value ...].",
|
|
32
|
+
":files": "List the session's scripts with their directives.",
|
|
33
|
+
":reinit": "Re-run #init scripts: :reinit [file.nadata].",
|
|
34
|
+
":auto": "Re-run the #auto scripts now.",
|
|
35
|
+
":flush": "Write all dirty instances to the world database.",
|
|
36
|
+
":quit": "Exit the console (also: :q, exit, quit).",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NadataConsole:
|
|
41
|
+
"""
|
|
42
|
+
Interactive console bound to a loaded Nadata database.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
app (Nadata): The loaded application the console operates on.
|
|
46
|
+
loader (NaDataLoader): Persistent loader whose scope carries state
|
|
47
|
+
across lines.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, app: Nadata) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Initializes the NadataConsole instance.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
app (Nadata): Loaded application (project folder, schema file,
|
|
56
|
+
or .ndb database).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
self.app: Nadata = app
|
|
60
|
+
self.loader: NaDataLoader = NaDataLoader(database=app.database)
|
|
61
|
+
|
|
62
|
+
# Initialize the persistent scope with the current database state
|
|
63
|
+
self.loader.load_file(content="")
|
|
64
|
+
|
|
65
|
+
def run(self) -> int:
|
|
66
|
+
"""
|
|
67
|
+
Runs the interactive read-eval-print loop.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
int: Process exit code (0 on normal exit).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
instance_count: int = self.app.total_instance_count()
|
|
74
|
+
|
|
75
|
+
print(
|
|
76
|
+
f"Nadata console - {len(self.app.struct_names)} struct(s), "
|
|
77
|
+
f"{instance_count} instance(s), {len(self.app.function_names)} function(s)."
|
|
78
|
+
)
|
|
79
|
+
print(
|
|
80
|
+
"Type an expression or statement, then a blank line to run it "
|
|
81
|
+
"(:help for commands)."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
buffer: list[str] = []
|
|
85
|
+
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
prompt: str = "nadata> " if not buffer else " ... "
|
|
89
|
+
line: str = input(prompt).strip()
|
|
90
|
+
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
# Cancel the current request, keep the session alive
|
|
93
|
+
if buffer:
|
|
94
|
+
buffer = []
|
|
95
|
+
print("(input cancelled)")
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
except EOFError:
|
|
103
|
+
print()
|
|
104
|
+
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
# Quit commands work immediately, even mid-request
|
|
108
|
+
if line in (":quit", ":q", "exit", "quit"):
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
# Commands execute immediately on a single ENTER
|
|
112
|
+
if not buffer and line.startswith(":"):
|
|
113
|
+
print(self._execute_command(command=line))
|
|
114
|
+
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Blank line: run the accumulated request (if any)
|
|
118
|
+
if not line:
|
|
119
|
+
if not buffer:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
source: str = "\n".join(buffer)
|
|
123
|
+
buffer = []
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
output: Optional[str] = self.execute_line(line=source)
|
|
127
|
+
|
|
128
|
+
if output is not None:
|
|
129
|
+
print(output)
|
|
130
|
+
|
|
131
|
+
except (
|
|
132
|
+
NameError,
|
|
133
|
+
AttributeError,
|
|
134
|
+
TypeError,
|
|
135
|
+
KeyError,
|
|
136
|
+
SyntaxError,
|
|
137
|
+
LoopLimitError,
|
|
138
|
+
) as error:
|
|
139
|
+
print(f"error: {error}")
|
|
140
|
+
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
buffer.append(line)
|
|
144
|
+
|
|
145
|
+
def execute_line(self, line: str) -> Optional[str]:
|
|
146
|
+
"""
|
|
147
|
+
Executes one request (single or multi line source) against the
|
|
148
|
+
live database.
|
|
149
|
+
|
|
150
|
+
A request holding exactly one bare expression echoes its formatted
|
|
151
|
+
value; anything else (declarations, assignments, loop bodies)
|
|
152
|
+
executes through the loader. print() and other None-returning calls
|
|
153
|
+
stay silent.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
line (str): The source request (lines joined with newlines).
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Optional[str]: The echoed value for expression requests, None
|
|
160
|
+
otherwise.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
if line.startswith(":"):
|
|
164
|
+
return self._execute_command(command=line)
|
|
165
|
+
|
|
166
|
+
statements: list[Statement] = self.loader.parse_statements(content=line)
|
|
167
|
+
|
|
168
|
+
if len(statements) == 1 and isinstance(statements[0], ExprStmt):
|
|
169
|
+
return self._evaluate_expression(expression=statements[0].expression)
|
|
170
|
+
|
|
171
|
+
self.loader.load_file(content=line)
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _evaluate_expression(self, expression: Expression) -> Optional[str]:
|
|
176
|
+
"""
|
|
177
|
+
Evaluates a single expression in the persistent scope.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
expression (Any): The expression AST node to evaluate.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Optional[str]: Formatted value, or None when the expression
|
|
184
|
+
returned no payload (e.g. print, setattr).
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
result: Value = expression.evaluate(scope=self.loader.scope)
|
|
188
|
+
|
|
189
|
+
if isinstance(result, ValueExternal) and result.payload is None:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
return format_runtime_value(val=result)
|
|
193
|
+
|
|
194
|
+
def _execute_command(self, command: str) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Handles a console meta command.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
command (str): The command as typed.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
str: The command output.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: On unknown commands.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
name: str = command.split(" ", 1)[0].strip()
|
|
209
|
+
|
|
210
|
+
handler: str = name
|
|
211
|
+
|
|
212
|
+
if handler == ":help":
|
|
213
|
+
lines: list[str] = ["Commands:"]
|
|
214
|
+
|
|
215
|
+
for cmd, description in CONSOLE_COMMANDS.items():
|
|
216
|
+
lines.append(f" {cmd:<10} {description}")
|
|
217
|
+
|
|
218
|
+
lines.append("")
|
|
219
|
+
lines.append("Anything else is evaluated as Nadata source.")
|
|
220
|
+
|
|
221
|
+
return "\n".join(lines)
|
|
222
|
+
|
|
223
|
+
if handler == ":info":
|
|
224
|
+
return self.app.summary()
|
|
225
|
+
|
|
226
|
+
if handler == ":classes":
|
|
227
|
+
return "\n".join(self.app.struct_names)
|
|
228
|
+
|
|
229
|
+
if handler == ":symbols":
|
|
230
|
+
return "\n".join(self.app.symbol_names)
|
|
231
|
+
|
|
232
|
+
if handler == ":flush":
|
|
233
|
+
written: int = self.app.database.flush()
|
|
234
|
+
|
|
235
|
+
return f"flushed {written} instance(s)."
|
|
236
|
+
|
|
237
|
+
if handler == ":run":
|
|
238
|
+
return self._run_script_command(arguments=command)
|
|
239
|
+
|
|
240
|
+
if handler == ":files":
|
|
241
|
+
return self._files_command()
|
|
242
|
+
|
|
243
|
+
if handler == ":reinit":
|
|
244
|
+
return self._reinit_command(arguments=command)
|
|
245
|
+
|
|
246
|
+
if handler == ":auto":
|
|
247
|
+
return self._auto_command()
|
|
248
|
+
|
|
249
|
+
raise ValueError(f"Unknown command '{command}'")
|
|
250
|
+
|
|
251
|
+
def _resolve_script(self, file_name: str) -> Any:
|
|
252
|
+
"""
|
|
253
|
+
Resolves a script path against the session folder.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
file_name (str): Script file name or path.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Any: The resolved ScriptFile.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
ValueError: If there is no folder session or no such file.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
from pathlib import Path
|
|
266
|
+
|
|
267
|
+
if self.app.session_folder is None:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
":run requires a folder session (nadata open <folder>)"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
folder: Path = Path(self.app.session_folder)
|
|
273
|
+
resolved: Path = Path(file_name)
|
|
274
|
+
|
|
275
|
+
if not resolved.is_file():
|
|
276
|
+
resolved = folder / file_name
|
|
277
|
+
|
|
278
|
+
if not resolved.is_file():
|
|
279
|
+
raise ValueError(f"Script file not found: '{file_name}'")
|
|
280
|
+
|
|
281
|
+
return parse_script_file(path=resolved, folder=folder)
|
|
282
|
+
|
|
283
|
+
def _run_script_command(self, arguments: str) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Handles ':run file.nadata [name=value ...]'.
|
|
286
|
+
|
|
287
|
+
Executes into the console's persistent scope so the script's
|
|
288
|
+
variables stay available afterwards (notebook feel).
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
arguments (str): Everything after ':run'.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
str: A one-line report.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValueError: On missing or mismatched parameters.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
import shlex
|
|
301
|
+
|
|
302
|
+
parts: list[str] = arguments.split(" ", 1)
|
|
303
|
+
rest: str = parts[1].strip() if len(parts) > 1 else ""
|
|
304
|
+
|
|
305
|
+
# posix=False keeps Windows backslashes in paths intact
|
|
306
|
+
tokens: list[str] = [
|
|
307
|
+
token.strip('"') for token in shlex.split(rest, posix=False)
|
|
308
|
+
] if rest else []
|
|
309
|
+
|
|
310
|
+
if not tokens:
|
|
311
|
+
raise ValueError("Usage: :run file.nadata [name=value ...]")
|
|
312
|
+
|
|
313
|
+
script: Any = self._resolve_script(file_name=tokens[0])
|
|
314
|
+
|
|
315
|
+
if script.kind != "manual":
|
|
316
|
+
raise ValueError(
|
|
317
|
+
f"'{script.rel_path}' is #{script.kind}: only #manual "
|
|
318
|
+
f"scripts run explicitly"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
from nadata.session import build_prelude
|
|
322
|
+
|
|
323
|
+
provided: dict[str, str] = {}
|
|
324
|
+
|
|
325
|
+
for token in tokens[1:]:
|
|
326
|
+
|
|
327
|
+
key: str
|
|
328
|
+
sep: str
|
|
329
|
+
value: str
|
|
330
|
+
key, sep, value = token.partition("=")
|
|
331
|
+
|
|
332
|
+
if not sep:
|
|
333
|
+
raise ValueError(
|
|
334
|
+
f"Malformed parameter '{token}' (expected name=value)"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
provided[key] = value
|
|
338
|
+
|
|
339
|
+
self.loader.prelude = build_prelude(
|
|
340
|
+
params=script.params,
|
|
341
|
+
provided=provided,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
self.loader.load_file(content=script.text)
|
|
346
|
+
|
|
347
|
+
finally:
|
|
348
|
+
self.loader.prelude = {}
|
|
349
|
+
|
|
350
|
+
return f"ran {script.rel_path}"
|
|
351
|
+
|
|
352
|
+
def _files_command(self) -> str:
|
|
353
|
+
"""
|
|
354
|
+
Handles ':files': lists the session's scripts and directives.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
str: The listing (or a note when there is no session).
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
from pathlib import Path
|
|
361
|
+
|
|
362
|
+
if self.app.session_folder is None:
|
|
363
|
+
return "No folder session (open a project folder)."
|
|
364
|
+
|
|
365
|
+
folder: Path = Path(self.app.session_folder)
|
|
366
|
+
_, scripts = scan_folder(folder=folder)
|
|
367
|
+
|
|
368
|
+
lines: list[str] = []
|
|
369
|
+
|
|
370
|
+
for script in scripts:
|
|
371
|
+
|
|
372
|
+
signature: str = ""
|
|
373
|
+
|
|
374
|
+
if script.kind == "manual" and script.params:
|
|
375
|
+
signature = "(" + ", ".join(
|
|
376
|
+
param.name + ": " + param.type_name
|
|
377
|
+
for param in script.params
|
|
378
|
+
) + ")"
|
|
379
|
+
|
|
380
|
+
lines.append(
|
|
381
|
+
f" #{script.kind:<7} {script.rel_path}{signature}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return "\n".join(lines) if lines else "No .nadata files."
|
|
385
|
+
|
|
386
|
+
def _reinit_command(self, arguments: str) -> str:
|
|
387
|
+
"""
|
|
388
|
+
Handles ':reinit [file.nadata]'.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
arguments (str): Everything after ':reinit'.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
str: A one-line report.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
ValueError: When there is no folder session or no match.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
from pathlib import Path
|
|
401
|
+
|
|
402
|
+
if self.app.session_folder is None or self.app.session_db is None:
|
|
403
|
+
raise ValueError(
|
|
404
|
+
":reinit requires a folder session (nadata open <folder>)"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
parts: list[str] = arguments.split(" ", 1)
|
|
408
|
+
target: str = parts[1].strip() if len(parts) > 1 else ""
|
|
409
|
+
|
|
410
|
+
folder: Path = Path(self.app.session_folder)
|
|
411
|
+
_, scripts = scan_folder(folder=folder)
|
|
412
|
+
|
|
413
|
+
ran: int = 0
|
|
414
|
+
|
|
415
|
+
for script in scripts:
|
|
416
|
+
|
|
417
|
+
if script.kind != "init":
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
if target and script.rel_path != target:
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
if target:
|
|
424
|
+
markers: Any = self.app.database.settings.setdefault(
|
|
425
|
+
"init_done", {}
|
|
426
|
+
)
|
|
427
|
+
markers.pop(script.marker_key(), None)
|
|
428
|
+
|
|
429
|
+
loader: NaDataLoader = NaDataLoader(
|
|
430
|
+
database=self.app.database
|
|
431
|
+
)
|
|
432
|
+
loader.load_file(content=script.text)
|
|
433
|
+
|
|
434
|
+
self.app.database.settings.setdefault("init_done", {})[
|
|
435
|
+
script.marker_key()
|
|
436
|
+
] = True
|
|
437
|
+
|
|
438
|
+
ran += 1
|
|
439
|
+
|
|
440
|
+
if target and ran == 0:
|
|
441
|
+
raise ValueError(f"No #init script matches '{target}'")
|
|
442
|
+
|
|
443
|
+
flush_dirty_instances(
|
|
444
|
+
database=self.app.database,
|
|
445
|
+
storage_path=self.app.session_db,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return f"re-ran {ran} init script(s)."
|
|
449
|
+
|
|
450
|
+
def _auto_command(self) -> str:
|
|
451
|
+
"""
|
|
452
|
+
Handles ':auto': re-runs every #auto script.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
str: A one-line report.
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
ValueError: When there is no folder session.
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
from pathlib import Path
|
|
462
|
+
|
|
463
|
+
if self.app.session_folder is None:
|
|
464
|
+
raise ValueError(
|
|
465
|
+
":auto requires a folder session (nadata open <folder>)"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
folder: Path = Path(self.app.session_folder)
|
|
469
|
+
_, scripts = scan_folder(folder=folder)
|
|
470
|
+
|
|
471
|
+
ran: int = 0
|
|
472
|
+
|
|
473
|
+
for script in scripts:
|
|
474
|
+
|
|
475
|
+
if script.kind != "auto":
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
loader: NaDataLoader = NaDataLoader(
|
|
479
|
+
database=self.app.database
|
|
480
|
+
)
|
|
481
|
+
loader.load_file(content=script.text)
|
|
482
|
+
|
|
483
|
+
ran += 1
|
|
484
|
+
|
|
485
|
+
return f"ran {ran} auto script(s)."
|
|
486
|
+
|
nadata/core/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database package initialization for Nadata.
|
|
3
|
+
|
|
4
|
+
Exports the core database components including EntityId, SlottedPage,
|
|
5
|
+
PageManager, StructTable, QueryOptimizer, QueryExecutor, and NaDataDatabase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import Modules
|
|
9
|
+
from nadata.core.database.builtins import PrintFunction
|
|
10
|
+
from nadata.core.database.dumper import NaDataDatabaseDumper
|
|
11
|
+
from nadata.core.database.queryset import QuerySet
|
|
12
|
+
from nadata.core.database.instance import StructInstance
|
|
13
|
+
from nadata.core.database.entity_id import EntityId
|
|
14
|
+
from nadata.core.database.slotted_page import SlottedPage
|
|
15
|
+
from nadata.core.database.page_manager import PageManager
|
|
16
|
+
from nadata.core.database.database import (
|
|
17
|
+
StructTable,
|
|
18
|
+
|
|
19
|
+
NaDataDatabase,
|
|
20
|
+
)
|
|
21
|
+
from nadata.core.database.query_engine import (
|
|
22
|
+
QueryExecutor,
|
|
23
|
+
QueryOptimizer,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__: list[str] = [
|
|
27
|
+
"NaDataDatabaseDumper",
|
|
28
|
+
"EntityId",
|
|
29
|
+
"QuerySet",
|
|
30
|
+
"SlottedPage",
|
|
31
|
+
"PageManager",
|
|
32
|
+
"StructTable",
|
|
33
|
+
"PrintFunction",
|
|
34
|
+
"StructInstance",
|
|
35
|
+
"NaDataDatabase",
|
|
36
|
+
"QueryExecutor",
|
|
37
|
+
"QueryOptimizer",
|
|
38
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in print function and output formatter for Nadata.
|
|
3
|
+
|
|
4
|
+
Provides the PrintFunction callable for execution scopes, formatting
|
|
5
|
+
primitives, Value wrappers, StructInstances, and QuerySets cleanly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import Modules
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from nadata.core.values.values import Value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PrintFunction:
|
|
18
|
+
"""
|
|
19
|
+
Built-in print implementation for Nadata runtime scopes.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
output_stream (Any): Output stream (defaults to sys.stdout).
|
|
23
|
+
captured_output (list[str]): Optional list capturing printed lines for testing.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
output_stream: Optional[Any] = None,
|
|
29
|
+
capture: bool = False
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Initializes the PrintFunction.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
output_stream (Optional[Any], optional): Target stream. Defaults to None (sys.stdout).
|
|
36
|
+
capture (bool, optional): Whether to capture lines in memory. Defaults to False.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
self.output_stream: Any = output_stream if output_stream is not None else sys.stdout
|
|
40
|
+
self.capture: bool = capture
|
|
41
|
+
self.captured_output: list[str] = []
|
|
42
|
+
|
|
43
|
+
def format_item(self, item: Any) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Formats a single item for readable printing.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
item (Any): Item to format.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: Human-readable string.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
if isinstance(item, Value):
|
|
55
|
+
raw = item.get()
|
|
56
|
+
|
|
57
|
+
if isinstance(raw, str):
|
|
58
|
+
return raw
|
|
59
|
+
|
|
60
|
+
return str(raw)
|
|
61
|
+
|
|
62
|
+
if hasattr(item, "__iter__") and not isinstance(item, (str, bytes)):
|
|
63
|
+
formatted_elements: list[str] = [self.format_item(x) for x in item]
|
|
64
|
+
return f"[{', '.join(formatted_elements)}]"
|
|
65
|
+
|
|
66
|
+
return str(item)
|
|
67
|
+
|
|
68
|
+
def __call__(self, *args: Any, **kwargs: Any) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Executes the print operation.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
args (Any): Positional arguments to print.
|
|
74
|
+
kwargs (Any): Keyword arguments (sep, end).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
sep: str = kwargs.get("sep", " ")
|
|
78
|
+
end: str = kwargs.get("end", "\n")
|
|
79
|
+
|
|
80
|
+
formatted_args: list[str] = [self.format_item(arg) for arg in args]
|
|
81
|
+
line: str = sep.join(formatted_args)
|
|
82
|
+
|
|
83
|
+
if self.capture:
|
|
84
|
+
self.captured_output.append(line)
|
|
85
|
+
|
|
86
|
+
self.output_stream.write(line + end)
|
|
87
|
+
self.output_stream.flush()
|