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.
Files changed (86) hide show
  1. nadata/__init__.py +15 -0
  2. nadata/api.py +894 -0
  3. nadata/cli.py +445 -0
  4. nadata/console.py +486 -0
  5. nadata/core/__init__.py +0 -0
  6. nadata/core/database/__init__.py +38 -0
  7. nadata/core/database/builtins.py +87 -0
  8. nadata/core/database/database.py +958 -0
  9. nadata/core/database/dumper.py +285 -0
  10. nadata/core/database/entity_id.py +142 -0
  11. nadata/core/database/instance.py +706 -0
  12. nadata/core/database/network_index.py +1142 -0
  13. nadata/core/database/page_manager.py +175 -0
  14. nadata/core/database/persistence/__init__.py +28 -0
  15. nadata/core/database/persistence/binary_cells.py +1141 -0
  16. nadata/core/database/persistence/compaction.py +97 -0
  17. nadata/core/database/persistence/dumper.py +798 -0
  18. nadata/core/database/persistence/lazy_instances.py +1537 -0
  19. nadata/core/database/persistence/loader.py +424 -0
  20. nadata/core/database/persistence/record_store.py +228 -0
  21. nadata/core/database/persistence/serialization.py +605 -0
  22. nadata/core/database/query_engine.py +222 -0
  23. nadata/core/database/queryset.py +508 -0
  24. nadata/core/database/reconciliation.py +287 -0
  25. nadata/core/database/slotted_page.py +190 -0
  26. nadata/core/database/spatial_index.py +1080 -0
  27. nadata/core/expressions/__init__.py +44 -0
  28. nadata/core/expressions/autograd.py +2909 -0
  29. nadata/core/expressions/builtins.py +4673 -0
  30. nadata/core/expressions/expression.py +2658 -0
  31. nadata/core/expressions/io_builtins.py +972 -0
  32. nadata/core/expressions/scope.py +336 -0
  33. nadata/core/expressions/statement.py +953 -0
  34. nadata/core/struct/__init__.py +14 -0
  35. nadata/core/struct/struct.py +218 -0
  36. nadata/core/struct/struct_attribute.py +100 -0
  37. nadata/core/struct/struct_method.py +324 -0
  38. nadata/core/types/__init__.py +33 -0
  39. nadata/core/types/bits/__init__.py +0 -0
  40. nadata/core/types/bits/bits.py +686 -0
  41. nadata/core/types/home_types/__init__.py +19 -0
  42. nadata/core/types/home_types/color.py +102 -0
  43. nadata/core/types/home_types/coordinate.py +1008 -0
  44. nadata/core/types/home_types/datetime.py +362 -0
  45. nadata/core/types/home_types/geogrid.py +92 -0
  46. nadata/core/types/home_types/geometry.py +1223 -0
  47. nadata/core/types/home_types/graph.py +0 -0
  48. nadata/core/types/home_types/planet.py +359 -0
  49. nadata/core/types/home_types/sea_level.py +98 -0
  50. nadata/core/types/home_types/vec.py +0 -0
  51. nadata/core/types/types.py +1345 -0
  52. nadata/core/values/__init__.py +14 -0
  53. nadata/core/values/values.py +1568 -0
  54. nadata/parser/__init__.py +0 -0
  55. nadata/parser/expressions/__init__.py +6 -0
  56. nadata/parser/expressions/dependencies.py +318 -0
  57. nadata/parser/expressions/expression_parser.py +993 -0
  58. nadata/parser/expressions/splitter.py +98 -0
  59. nadata/parser/expressions/statement_parser.py +1425 -0
  60. nadata/parser/expressions/tokenizer.py +402 -0
  61. nadata/parser/expressions/type_parser.py +187 -0
  62. nadata/parser/nadata_loader/__init__.py +8 -0
  63. nadata/parser/nadata_loader/loader.py +951 -0
  64. nadata/parser/nastruct_parser/__init__.py +0 -0
  65. nadata/parser/nastruct_parser/declaration.py +105 -0
  66. nadata/parser/nastruct_parser/imports.py +164 -0
  67. nadata/parser/nastruct_parser/nastruct_parser.py +209 -0
  68. nadata/parser/nastruct_parser/parse_attributes.py +149 -0
  69. nadata/parser/nastruct_parser/parse_callables.py +436 -0
  70. nadata/parser/nastruct_parser/parse_expression.py +59 -0
  71. nadata/parser/nastruct_parser/parse_structs.py +206 -0
  72. nadata/parser/nastruct_parser/parser_state.py +84 -0
  73. nadata/parser/nastruct_parser/resolver.py +1412 -0
  74. nadata/parser/nastruct_parser/struct_body_state.py +50 -0
  75. nadata/parser/nastruct_parser/tmp_structs.py +156 -0
  76. nadata/parser/utils/__init__.py +20 -0
  77. nadata/parser/utils/brackets.py +252 -0
  78. nadata/parser/utils/comments_remove.py +149 -0
  79. nadata/parser/utils/identifiers.py +121 -0
  80. nadata/session.py +868 -0
  81. nadata-0.1.0.dist-info/METADATA +191 -0
  82. nadata-0.1.0.dist-info/RECORD +86 -0
  83. nadata-0.1.0.dist-info/WHEEL +5 -0
  84. nadata-0.1.0.dist-info/entry_points.txt +2 -0
  85. nadata-0.1.0.dist-info/licenses/LICENSE +21 -0
  86. 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
+
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()