amalgam-lang 13.0.3__py3-none-manylinux_2_28_x86_64.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.

Potentially problematic release.


This version of amalgam-lang might be problematic. Click here for more details.

amalgam/api.py ADDED
@@ -0,0 +1,1108 @@
1
+ from __future__ import annotations
2
+
3
+ from ctypes import (
4
+ _Pointer, Array, byref, c_bool, c_char, c_char_p, c_double, c_size_t, c_uint64, c_void_p,
5
+ cast, cdll, POINTER, Structure
6
+ )
7
+ from datetime import datetime
8
+ import gc
9
+ import json as json_lib
10
+ import logging
11
+ from pathlib import Path
12
+ import platform
13
+ import re
14
+ import typing as t
15
+ import warnings
16
+
17
+ # Set to amalgam
18
+ _logger = logging.getLogger('amalgam')
19
+
20
+
21
+ class _LoadEntityStatus(Structure):
22
+ """
23
+ A private status returned from Amalgam binary LoadEntity C API.
24
+
25
+ This is implemented with ctypes for accessing binary Amalgam builds.
26
+ """
27
+
28
+ _fields_ = [
29
+ ("loaded", c_bool),
30
+ ("message", POINTER(c_char)),
31
+ ("version", POINTER(c_char))
32
+ ]
33
+
34
+
35
+ class LoadEntityStatus:
36
+ """
37
+ Status returned by :func:`~api.Amalgam.load_entity`.
38
+
39
+ This is implemented with python types and is meant to wrap _LoadEntityStatus
40
+ which uses ctypes and directly interacts with the Amalgam binaries.
41
+
42
+ Parameters
43
+ ----------
44
+ api : Amalgam
45
+ The Python Amalgam interface.
46
+ c_status : _LoadEntityStatus, optional
47
+ _LoadEntityStatus instance.
48
+ """
49
+
50
+ def __init__(self, api: Amalgam, c_status: t.Optional[_LoadEntityStatus] = None):
51
+ """Initialize LoadEntityStatus."""
52
+ if c_status is None:
53
+ self.loaded = True
54
+ self.message = ""
55
+ self.version = ""
56
+ else:
57
+ self.loaded = bool(c_status.loaded)
58
+ self.message = api.char_p_to_bytes(c_status.message).decode("utf-8")
59
+ self.version = api.char_p_to_bytes(c_status.version).decode("utf-8")
60
+
61
+ def __str__(self) -> str:
62
+ """
63
+ Return a human-readable string representation.
64
+
65
+ Returns
66
+ -------
67
+ str
68
+ The human-readable string representation.
69
+ """
70
+ return f"{self.loaded},\"{self.message}\",\"{self.version}\""
71
+
72
+
73
+ class Amalgam:
74
+ """
75
+ A general python direct interface to the Amalgam library.
76
+
77
+ This is implemented with ctypes for accessing binary Amalgam builds.
78
+
79
+ Parameters
80
+ ----------
81
+ library_path : Path or str, optional
82
+ Path to either the amalgam DLL, DyLib or SO (Windows, MacOS
83
+ or Linux, respectively). If not specified it will build a path to the
84
+ appropriate library bundled with the package.
85
+
86
+ append_trace_file : bool, default False
87
+ If True, new content will be appended to a trace file if the file
88
+ already exists rather than creating a new file.
89
+
90
+ arch : str, optional
91
+ The platform architecture of the embedded Amalgam library.
92
+ If not provided, it will be automatically detected.
93
+ (Note: arm64_8a architecture must be manually specified!)
94
+
95
+ execution_trace_dir : str, optional
96
+ A directory path for writing trace files. If ``None``, then
97
+ the current working directory will be used.
98
+
99
+ execution_trace_file : str, default "execution.trace"
100
+ The full or relative path to the execution trace used in debugging.
101
+
102
+ gc_interval : int, optional
103
+ If set, garbage collection will be forced at the specified
104
+ interval of amalgam operations. Note that this reduces memory
105
+ consumption at the compromise of performance. Only use if models are
106
+ exceeding your host's process memory limit or if paging to disk. As an
107
+ example, if this operation is set to 0 (force garbage collection every
108
+ operation), it results in a performance impact of 150x.
109
+ Default value does not force garbage collection.
110
+
111
+ library_postfix : str, optional
112
+ For configuring use of different amalgam builds i.e. -st for
113
+ single-threaded. If not provided, an attempt will be made to detect
114
+ it within library_path. If neither are available, -mt (multi-threaded)
115
+ will be used.
116
+
117
+ max_num_threads : int, optional
118
+ If a multithreaded Amalgam binary is used, sets the maximum
119
+ number of threads to the value specified. If 0, will use the number of
120
+ visible logical cores. Default None will not attempt to set this value.
121
+
122
+ sbf_datastore_enabled : bool, optional
123
+ If true, sbf tree structures are enabled.
124
+
125
+ trace : bool, optional
126
+ If true, enables execution trace file.
127
+
128
+ Raises
129
+ ------
130
+ FileNotFoundError
131
+ Amalgam library not found in default location, and not configured to
132
+ retrieve automatically.
133
+ RuntimeError
134
+ The initializer was unable to determine a supported platform or
135
+ architecture to use when no explicit `library_path` was supplied.
136
+ """
137
+
138
+ def __init__( # noqa: C901
139
+ self,
140
+ library_path: t.Optional[Path | str] = None,
141
+ *,
142
+ arch: t.Optional[str] = None,
143
+ append_trace_file: bool = False,
144
+ execution_trace_dir: t.Optional[str] = None,
145
+ execution_trace_file: str = "execution.trace",
146
+ gc_interval: t.Optional[int] = None,
147
+ library_postfix: t.Optional[str] = None,
148
+ max_num_threads: t.Optional[int] = None,
149
+ sbf_datastore_enabled: t.Optional[bool] = None,
150
+ trace: t.Optional[bool] = None,
151
+ **kwargs
152
+ ):
153
+ """Initialize Amalgam instance."""
154
+ if len(kwargs):
155
+ warnings.warn(f'Unexpected keyword arguments '
156
+ f'[{", ".join(list(kwargs.keys()))}] '
157
+ f'passed to Amalgam constructor.')
158
+
159
+ # Work out path and postfix of the library from the given parameters.
160
+ self.library_path, self.library_postfix = self._get_library_path(
161
+ library_path, library_postfix, arch)
162
+
163
+ self.append_trace_file = append_trace_file
164
+ if trace:
165
+ # Determine where to put the trace files ...
166
+ self.base_execution_trace_file = execution_trace_file
167
+ # default to current directory, and expand relative paths ..
168
+ if execution_trace_dir is None:
169
+ self.execution_trace_dir = Path.cwd()
170
+ else:
171
+ self.execution_trace_dir = Path(
172
+ execution_trace_dir).expanduser().absolute()
173
+ # Create the trace directory if needed
174
+ if not self.execution_trace_dir.exists():
175
+ self.execution_trace_dir.mkdir(parents=True, exist_ok=True)
176
+
177
+ # increment a counter on the file name, if file already exists..
178
+ self.execution_trace_filepath = Path(
179
+ self.execution_trace_dir, execution_trace_file)
180
+ if not self.append_trace_file:
181
+ counter = 1
182
+ while self.execution_trace_filepath.exists():
183
+ self.execution_trace_filepath = Path(
184
+ self.execution_trace_dir,
185
+ f'{self.base_execution_trace_file}.{counter}'
186
+ )
187
+ counter += 1
188
+
189
+ self.trace = open(self.execution_trace_filepath, 'w+',
190
+ encoding='utf-8')
191
+ _logger.debug("Opening Amalgam trace file: "
192
+ f"{self.execution_trace_filepath}")
193
+ else:
194
+ self.trace = None
195
+
196
+ _logger.debug(f"Loading amalgam library: {self.library_path}")
197
+ _logger.debug(f"SBF_DATASTORE enabled: {sbf_datastore_enabled}")
198
+ self.amlg = cdll.LoadLibrary(str(self.library_path))
199
+ if sbf_datastore_enabled is not None:
200
+ self.set_amlg_flags(sbf_datastore_enabled)
201
+ if max_num_threads is not None:
202
+ self.set_max_num_threads(max_num_threads)
203
+ self.gc_interval = gc_interval
204
+ self.op_count = 0
205
+ self.load_command_log_entry = None
206
+
207
+ @classmethod
208
+ def _get_allowed_postfixes(cls, library_dir: Path) -> list[str]:
209
+ """
210
+ Return list of all library postfixes allowed given library directory.
211
+
212
+ Parameters
213
+ ----------
214
+ library_dir : Path
215
+ The path object to the library directory.
216
+
217
+ Returns
218
+ -------
219
+ list of str
220
+ The allowed library postfixes.
221
+ """
222
+ allowed_postfixes = set()
223
+ for file in library_dir.glob("amalgam*"):
224
+ postfix = cls._parse_postfix(file.name)
225
+ if postfix is not None:
226
+ allowed_postfixes.add(postfix)
227
+ return list(allowed_postfixes)
228
+
229
+ @classmethod
230
+ def _parse_postfix(cls, filename: str) -> str | None:
231
+ """
232
+ Determine library postfix given a filename.
233
+
234
+ Parameters
235
+ ----------
236
+ filename : str
237
+ The filename to parse.
238
+
239
+ Returns
240
+ -------
241
+ str or None
242
+ The library postfix of the filename, or None if no postfix.
243
+ """
244
+ matches = re.findall(r'-([^.]+)(?:\.[^.]*)?$', filename)
245
+ if len(matches) > 0:
246
+ return f'-{matches[-1]}'
247
+ else:
248
+ return None
249
+
250
+ @classmethod
251
+ def _get_library_path(
252
+ cls,
253
+ library_path: t.Optional[Path | str] = None,
254
+ library_postfix: t.Optional[str] = None,
255
+ arch: t.Optional[str] = None
256
+ ) -> tuple[Path, str]:
257
+ """
258
+ Return the full Amalgam library path and its library_postfix.
259
+
260
+ Using the potentially empty parameters passed into the initializer,
261
+ determine and return the prescribed or the correct default path and
262
+ library postfix for the running environment.
263
+
264
+ Parameters
265
+ ----------
266
+ library_path : Path or str, optional
267
+ The path to the Amalgam shared library.
268
+ library_postfix : str, optional
269
+ The library type as specified by a postfix to the word
270
+ "amalgam" in the library's filename. E.g., the "-mt" in
271
+ `amalgam-mt.dll`. If left unspecified, "-mt" will be used where
272
+ supported, otherwise "-st".
273
+ arch : str, optional
274
+ The platform architecture of the embedded Amalgam
275
+ library. If not provided, it will be automatically detected.
276
+ (Note: arm64_8a architecture must be manually specified!)
277
+
278
+ Returns
279
+ -------
280
+ Path
281
+ The path to the appropriate Amalgam shared lib (.dll, .so, .dylib).
282
+ str
283
+ The library postfix.
284
+ """
285
+ if library_postfix and not library_postfix.startswith("-"):
286
+ # Library postfix must start with a dash
287
+ raise ValueError(
288
+ f'The provided `library_postfix` value of "{library_postfix}" '
289
+ 'must start with a "-".'
290
+ )
291
+
292
+ if library_path:
293
+ # Find the library postfix, if one is present in the given
294
+ # library_path.
295
+ filename = Path(library_path).name
296
+ _library_postfix = cls._parse_postfix(filename)
297
+ if library_postfix and library_postfix != _library_postfix:
298
+ warnings.warn(
299
+ 'The supplied `library_postfix` does not match the '
300
+ 'postfix given in `library_path` and will be ignored.',
301
+ UserWarning
302
+ )
303
+ library_postfix = _library_postfix
304
+ library_path = Path(library_path).expanduser()
305
+
306
+ if not library_path.exists():
307
+ raise FileNotFoundError(
308
+ 'No Amalgam library was found at the provided '
309
+ f'`library_path`: "{library_path}". Please check that the '
310
+ 'path is correct.'
311
+ )
312
+ else:
313
+ # No library_path was provided so, auto-determine the correct one
314
+ # to use for this running environment. For this, the operating
315
+ # system, the machine architecture and postfix are used.
316
+ os = platform.system().lower()
317
+
318
+ arch_supported = False
319
+ if not arch:
320
+ arch = platform.machine().lower()
321
+
322
+ if arch == 'x86_64':
323
+ arch = 'amd64'
324
+ elif arch.startswith('aarch64') or arch.startswith('arm64'):
325
+ # see: https://stackoverflow.com/q/45125516/440805
326
+ arch = 'arm64'
327
+
328
+ if os == 'windows':
329
+ path_os = 'windows'
330
+ path_ext = 'dll'
331
+ arch_supported = arch in ['amd64']
332
+ elif os == "darwin":
333
+ path_os = 'darwin'
334
+ path_ext = 'dylib'
335
+ arch_supported = arch in ['amd64', 'arm64']
336
+ elif os == "linux":
337
+ path_os = 'linux'
338
+ path_ext = 'so'
339
+ arch_supported = arch in ['amd64', 'arm64', 'arm64_8a']
340
+ else:
341
+ raise RuntimeError(
342
+ f'Detected an unsupported machine platform type "{os}". '
343
+ 'Please specify the `library_path` to the Amalgam shared '
344
+ 'library to use with this platform.')
345
+
346
+ if not arch_supported:
347
+ raise RuntimeError(
348
+ f'An unsupported machine architecture "{arch}" was '
349
+ 'detected or provided. Please specify the `library_path` '
350
+ 'to the Amalgam shared library to use with this machine '
351
+ 'architecture.')
352
+
353
+ if not library_postfix:
354
+ library_postfix = '-mt' if arch != "arm64_8a" else '-st'
355
+
356
+ # Default path for Amalgam binary should be at <package_root>/lib
357
+ lib_root = Path(Path(__file__).parent, 'lib')
358
+
359
+ # Build path
360
+ dir_path = Path(lib_root, path_os, arch)
361
+ filename = f'amalgam{library_postfix}.{path_ext}'
362
+ library_path = Path(dir_path, filename)
363
+
364
+ if not library_path.exists():
365
+ # First check if invalid postfix, otherwise show generic error
366
+ allowed_postfixes = cls._get_allowed_postfixes(dir_path)
367
+ _library_postfix = cls._parse_postfix(filename)
368
+ if (
369
+ allowed_postfixes and
370
+ _library_postfix not in allowed_postfixes
371
+ ):
372
+ raise RuntimeError(
373
+ 'An unsupported `library_postfix` value of '
374
+ f'"{_library_postfix}" was provided. Supported options '
375
+ "for your machine's platform and architecture include: "
376
+ f'{", ".join(allowed_postfixes)}.'
377
+ )
378
+ raise FileNotFoundError(
379
+ 'The auto-determined Amalgam library to use was not found '
380
+ f'at "{library_path}". This could indicate that the '
381
+ 'combination of operating system, machine architecture and '
382
+ 'library-postfix is not yet supported.'
383
+ )
384
+
385
+ return library_path, library_postfix
386
+
387
+ def is_sbf_datastore_enabled(self) -> bool:
388
+ """
389
+ Return whether the SBF Datastore is implemented.
390
+
391
+ Returns
392
+ -------
393
+ bool
394
+ True if sbf tree structures are currently enabled.
395
+ """
396
+ self.amlg.IsSBFDataStoreEnabled.restype = c_bool
397
+ return self.amlg.IsSBFDataStoreEnabled()
398
+
399
+ def set_amlg_flags(self, sbf_datastore_enabled: bool = True):
400
+ """
401
+ Set various amalgam flags for data structure and compute features.
402
+
403
+ Parameters
404
+ ----------
405
+ sbf_datastore_enabled : bool, default True
406
+ If true, sbf tree structures are enabled.
407
+ """
408
+ self.amlg.SetSBFDataStoreEnabled.argtypes = [c_bool]
409
+ self.amlg.SetSBFDataStoreEnabled.restype = c_void_p
410
+ self.amlg.SetSBFDataStoreEnabled(sbf_datastore_enabled)
411
+
412
+ def get_max_num_threads(self) -> int:
413
+ """
414
+ Get the maximum number of threads currently set.
415
+
416
+ Returns
417
+ -------
418
+ int
419
+ The maximum number of threads that Amalgam is configured to use.
420
+ """
421
+ self.amlg.GetMaxNumThreads.restype = c_size_t
422
+ self._log_execution("GET_MAX_NUM_THREADS")
423
+ result = self.amlg.GetMaxNumThreads()
424
+ self._log_reply(result)
425
+
426
+ return result
427
+
428
+ def set_max_num_threads(self, max_num_threads: int = 0):
429
+ """
430
+ Set the maximum number of threads.
431
+
432
+ Will have no effect if a single-threaded version of Amalgam is used.
433
+
434
+ Parameters
435
+ ----------
436
+ max_num_threads : int, default 0
437
+ If a multithreaded Amalgam binary is used, sets the maximum number
438
+ of threads to the value specified. If 0, will use the number of
439
+ visible logical cores.
440
+ """
441
+ self.amlg.SetMaxNumThreads.argtypes = [c_size_t]
442
+ self.amlg.SetMaxNumThreads.restype = c_void_p
443
+
444
+ self._log_execution(f"SET_MAX_NUM_THREADS {max_num_threads}")
445
+ result = self.amlg.SetMaxNumThreads(max_num_threads)
446
+ self._log_reply(result)
447
+
448
+ def reset_trace(self, file: str):
449
+ """
450
+ Close the open trace file and opens a new one with the specified name.
451
+
452
+ Parameters
453
+ ----------
454
+ file : str
455
+ The file name for the new execution trace.
456
+ """
457
+ if self.trace is None:
458
+ # Trace was not enabled
459
+ return
460
+ _logger.debug(f"Execution trace file being reset: "
461
+ f"{self.execution_trace_filepath} to be closed ...")
462
+ # Write exit command.
463
+ self.trace.write("EXIT\n")
464
+ self.trace.close()
465
+ self.execution_trace_filepath = Path(self.execution_trace_dir, file)
466
+
467
+ # increment a counter on the file name, if file already exists..
468
+ if not self.append_trace_file:
469
+ counter = 1
470
+ while self.execution_trace_filepath.exists():
471
+ self.execution_trace_filepath = Path(
472
+ self.execution_trace_dir, f'{file}.{counter}')
473
+ counter += 1
474
+
475
+ self.trace = open(self.execution_trace_filepath, 'w+')
476
+ _logger.debug(f"New trace file: {self.execution_trace_filepath} "
477
+ f"opened.")
478
+ # Write load command used to instantiate the amalgam instance.
479
+ if self.load_command_log_entry is not None:
480
+ self.trace.write(self.load_command_log_entry + "\n")
481
+ self.trace.flush()
482
+
483
+ def __str__(self) -> str:
484
+ """Return a human-readable string representation."""
485
+ return (f"Amalgam Path:\t\t {self.library_path}\n"
486
+ f"Amalgam GC Interval:\t {self.gc_interval}\n")
487
+
488
+ def __del__(self):
489
+ """Implement a "destructor" method to finalize log files, if any."""
490
+ if (
491
+ getattr(self, 'debug', False) and
492
+ getattr(self, 'trace', None) is not None
493
+ ):
494
+ try:
495
+ self.trace.write("EXIT\n")
496
+ except Exception: # noqa - deliberately broad
497
+ pass
498
+
499
+ def _log_comment(self, comment: str):
500
+ """
501
+ Log a comment into the execution trace file.
502
+
503
+ Allows notes of information not captured in the raw execution commands.
504
+
505
+ Parameters
506
+ ----------
507
+ reply : str
508
+ The raw reply string to log.
509
+ """
510
+ if self.trace:
511
+ self.trace.write("# NOTE >" + str(comment) + "\n")
512
+ self.trace.flush()
513
+
514
+ def _log_reply(self, reply: t.Any):
515
+ """
516
+ Log a raw reply from the amalgam process.
517
+
518
+ Uses a pre-pended '#RESULT >' so it can be filtered by tools like grep.
519
+
520
+ Parameters
521
+ ----------
522
+ reply : Any
523
+ The raw reply string to log.
524
+ """
525
+ if self.trace:
526
+ self.trace.write("# RESULT >" + str(reply) + "\n")
527
+ self.trace.flush()
528
+
529
+ def _log_time(self, label: str):
530
+ """
531
+ Log a labelled timestamp to the trace file.
532
+
533
+ Parameters
534
+ ----------
535
+ label: str
536
+ A string to annotate the timestamped trace entry
537
+ """
538
+ if self.trace:
539
+ dt = datetime.now()
540
+ self.trace.write(f"# TIME {label} {dt:%Y-%m-%d %H:%M:%S},"
541
+ f"{f'{dt:%f}'[:3]}\n")
542
+ self.trace.flush()
543
+
544
+ def _log_execution(self, execution_string: str):
545
+ """
546
+ Log an execution string.
547
+
548
+ Logs an execution string that is sent to the amalgam process for use in
549
+ command line debugging.
550
+
551
+ Parameters
552
+ ----------
553
+ execution_string : str
554
+ A formatted string that can be piped into an amalgam command line
555
+ process for use in debugging.
556
+
557
+ .. NOTE::
558
+ No formatting checks are performed, it is assumed the execution
559
+ string passed is valid.
560
+ """
561
+ if self.trace:
562
+ self.trace.write(execution_string + "\n")
563
+ self.trace.flush()
564
+
565
+ def gc(self):
566
+ """Force garbage collection when called if self.force_gc is set."""
567
+ if (
568
+ self.gc_interval is not None
569
+ and self.op_count > self.gc_interval
570
+ ):
571
+ _logger.debug("Collecting Garbage")
572
+ gc.collect()
573
+ self.op_count = 0
574
+ self.op_count += 1
575
+
576
+ def str_to_char_p(
577
+ self,
578
+ value: str | bytes,
579
+ size: t.Optional[int] = None
580
+ ) -> Array[c_char]:
581
+ """
582
+ Convert a string to an Array of C char.
583
+
584
+ User must call `del` on returned buffer
585
+
586
+ Parameters
587
+ ----------
588
+ value : str or bytes
589
+ The value of the string.
590
+ size : int, optional
591
+ The size of the string. If not provided, the length of
592
+ the string is used.
593
+
594
+ Returns
595
+ -------
596
+ Array of c_char
597
+ An Array of C char datatypes which form the given string
598
+ """
599
+ if isinstance(value, str):
600
+ value = value.encode('utf-8')
601
+ buftype = c_char * (size if size is not None else (len(value) + 1))
602
+ buf = buftype()
603
+ buf.value = value
604
+ return buf
605
+
606
+ def char_p_to_bytes(self, p: _Pointer[c_char] | c_char_p) -> bytes | None:
607
+ """
608
+ Copy native C char pointer to bytes, cleaning up memory correctly.
609
+
610
+ Parameters
611
+ ----------
612
+ p : c_char_p
613
+ The char pointer to convert
614
+
615
+ Returns
616
+ -------
617
+ bytes or None
618
+ The byte-encoded char
619
+ """
620
+ bytes_str = cast(p, c_char_p).value
621
+
622
+ self.amlg.DeleteString.argtypes = [c_char_p]
623
+ self.amlg.DeleteString.restype = None
624
+ self.amlg.DeleteString(p)
625
+
626
+ return bytes_str
627
+
628
+ def get_json_from_label(self, handle: str, label: str) -> bytes:
629
+ """
630
+ Get a label from amalgam and returns it in json format.
631
+
632
+ Parameters
633
+ ----------
634
+ handle : str
635
+ The handle of the amalgam entity.
636
+ label : str
637
+ The label to retrieve.
638
+
639
+ Returns
640
+ -------
641
+ bytes
642
+ The byte-encoded json representation of the amalgam label.
643
+ """
644
+ self.amlg.GetJSONPtrFromLabel.restype = POINTER(c_char)
645
+ self.amlg.GetJSONPtrFromLabel.argtypes = [c_char_p, c_char_p]
646
+ handle_buf = self.str_to_char_p(handle)
647
+ label_buf = self.str_to_char_p(label)
648
+
649
+ self._log_execution((
650
+ f"GET_JSON_FROM_LABEL \"{self.escape_double_quotes(handle)}\" "
651
+ f"\"{self.escape_double_quotes(label)}\""
652
+ ))
653
+ result = self.char_p_to_bytes(self.amlg.GetJSONPtrFromLabel(handle_buf, label_buf))
654
+ self._log_reply(result)
655
+
656
+ del handle_buf
657
+ del label_buf
658
+ self.gc()
659
+
660
+ return result
661
+
662
+ def set_json_to_label(
663
+ self,
664
+ handle: str,
665
+ label: str,
666
+ json: str | bytes
667
+ ):
668
+ """
669
+ Set a label in amalgam using json.
670
+
671
+ Parameters
672
+ ----------
673
+ handle : str
674
+ The handle of the amalgam entity.
675
+ label : str
676
+ The label to set.
677
+ json : str or bytes
678
+ The json representation of the label value.
679
+ """
680
+ self.amlg.SetJSONToLabel.restype = c_void_p
681
+ self.amlg.SetJSONToLabel.argtypes = [c_char_p, c_char_p, c_char_p]
682
+ handle_buf = self.str_to_char_p(handle)
683
+ label_buf = self.str_to_char_p(label)
684
+ json_buf = self.str_to_char_p(json)
685
+
686
+ self._log_execution((
687
+ f"SET_JSON_TO_LABEL \"{self.escape_double_quotes(handle)}\" "
688
+ f"\"{self.escape_double_quotes(label)}\" "
689
+ f"{json}"
690
+ ))
691
+ self.amlg.SetJSONToLabel(handle_buf, label_buf, json_buf)
692
+ self._log_reply(None)
693
+
694
+ del handle_buf
695
+ del label_buf
696
+ del json_buf
697
+ self.gc()
698
+
699
+ def load_entity(
700
+ self,
701
+ handle: str,
702
+ file_path: str,
703
+ *,
704
+ file_type: str = "",
705
+ persist: bool = False,
706
+ json_file_params: str = "",
707
+ write_log: str = "",
708
+ print_log: str = ""
709
+ ) -> LoadEntityStatus:
710
+ """
711
+ Load an entity from an amalgam source file.
712
+
713
+ Parameters
714
+ ----------
715
+ handle : str
716
+ The handle to assign the entity.
717
+ file_path : str
718
+ The path of the file name to load.
719
+ file_type : str, default ""
720
+ If set to a nonempty string, will represent the type of file to load.
721
+ persist : bool, default False
722
+ If set to true, all transactions that update the entity will also be
723
+ written to the files.
724
+ json_file_params : str, default ""
725
+ Either empty string or a string of json specifying a set of key-value pairs
726
+ which are parameters specific to the file type. See Amalgam documentation
727
+ for details of allowed parameters.
728
+ write_log : str, default ""
729
+ Path to the write log. If empty string, the write log is
730
+ not generated.
731
+ print_log : str, default ""
732
+ Path to the print log. If empty string, the print log is
733
+ not generated.
734
+
735
+ Returns
736
+ -------
737
+ LoadEntityStatus
738
+ Status of LoadEntity call.
739
+ """
740
+ self.amlg.LoadEntity.argtypes = [
741
+ c_char_p, c_char_p, c_char_p, c_bool, c_char_p, c_char_p, c_char_p]
742
+ self.amlg.LoadEntity.restype = _LoadEntityStatus
743
+ handle_buf = self.str_to_char_p(handle)
744
+ file_path_buf = self.str_to_char_p(file_path)
745
+ file_type_buf = self.str_to_char_p(file_type)
746
+ json_file_params_buf = self.str_to_char_p(json_file_params)
747
+ write_log_buf = self.str_to_char_p(write_log)
748
+ print_log_buf = self.str_to_char_p(print_log)
749
+
750
+ load_command_log_entry = (
751
+ f"LOAD_ENTITY \"{self.escape_double_quotes(handle)}\" "
752
+ f"\"{self.escape_double_quotes(file_path)}\" "
753
+ f"\"{self.escape_double_quotes(file_type)}\" {str(persist).lower()} "
754
+ f"{json_lib.dumps(json_file_params)} "
755
+ f"\"{write_log}\" \"{print_log}\""
756
+ )
757
+ self._log_execution(load_command_log_entry)
758
+ result = LoadEntityStatus(self, self.amlg.LoadEntity(
759
+ handle_buf, file_path_buf, file_type_buf, persist,
760
+ json_file_params_buf, write_log_buf, print_log_buf))
761
+ self._log_reply(result)
762
+
763
+ del handle_buf
764
+ del file_path_buf
765
+ del file_type_buf
766
+ del json_file_params_buf
767
+ del write_log_buf
768
+ del print_log_buf
769
+ self.gc()
770
+
771
+ return result
772
+
773
+ def verify_entity(
774
+ self,
775
+ file_path: str
776
+ ) -> LoadEntityStatus:
777
+ """
778
+ Verify an entity from an amalgam source file.
779
+
780
+ Parameters
781
+ ----------
782
+ file_path : str
783
+ The path to the filename.amlg/caml file.
784
+
785
+ Returns
786
+ -------
787
+ LoadEntityStatus
788
+ Status of VerifyEntity call.
789
+ """
790
+ self.amlg.VerifyEntity.argtypes = [c_char_p]
791
+ self.amlg.VerifyEntity.restype = _LoadEntityStatus
792
+ file_path_buf = self.str_to_char_p(file_path)
793
+
794
+ self._log_execution(f"VERIFY_ENTITY \"{self.escape_double_quotes(file_path)}\"")
795
+ result = LoadEntityStatus(self, self.amlg.VerifyEntity(file_path_buf))
796
+ self._log_reply(result)
797
+
798
+ del file_path_buf
799
+ self.gc()
800
+
801
+ return result
802
+
803
+ def clone_entity(
804
+ self,
805
+ handle: str,
806
+ clone_handle: str,
807
+ *,
808
+ file_path: str = "",
809
+ file_type: str = "",
810
+ persist: bool = False,
811
+ json_file_params: str = "",
812
+ write_log: str = "",
813
+ print_log: str = ""
814
+ ) -> bool:
815
+ """
816
+ Clones entity specified by handle into a new entity specified by clone_handle.
817
+
818
+ Parameters
819
+ ----------
820
+ handle : str
821
+ The handle of the amalgam entity to clone.
822
+ clone_handle : str
823
+ The handle to clone the entity into.
824
+ file_path : str, default ""
825
+ The path of the file name to load.
826
+ file_type : str, default ""
827
+ If set to a nonempty string, will represent the type of file to load.
828
+ persist : bool, default False
829
+ If set to true, all transactions that update the entity will also be
830
+ written to the files.
831
+ json_file_params : str, default ""
832
+ Either empty string or a string of json specifying a set of key-value pairs
833
+ which are parameters specific to the file type. See Amalgam documentation
834
+ for details of allowed parameters.
835
+ write_log : str, default ""
836
+ Path to the write log. If empty string, the write log is
837
+ not generated.
838
+ print_log : str, default ""
839
+ Path to the print log. If empty string, the print log is
840
+ not generated.
841
+
842
+ Returns
843
+ -------
844
+ bool
845
+ True if cloned successfully, False if not.
846
+ """
847
+ self.amlg.CloneEntity.argtypes = [
848
+ c_char_p, c_char_p, c_char_p, c_char_p, c_bool, c_char_p, c_char_p, c_char_p]
849
+ handle_buf = self.str_to_char_p(handle)
850
+ clone_handle_buf = self.str_to_char_p(clone_handle)
851
+ file_path_buf = self.str_to_char_p(file_path)
852
+ file_type_buf = self.str_to_char_p(file_type)
853
+ json_file_params_buf = self.str_to_char_p(json_file_params)
854
+ write_log_buf = self.str_to_char_p(write_log)
855
+ print_log_buf = self.str_to_char_p(print_log)
856
+
857
+ clone_command_log_entry = (
858
+ f'CLONE_ENTITY "{self.escape_double_quotes(handle)}" '
859
+ f'"{self.escape_double_quotes(clone_handle)}" '
860
+ f"\"{self.escape_double_quotes(file_path)}\" "
861
+ f"\"{self.escape_double_quotes(file_type)}\" {str(persist).lower()} "
862
+ f"{json_lib.dumps(json_file_params)} "
863
+ f"\"{write_log}\" \"{print_log}\""
864
+ )
865
+ self._log_execution(clone_command_log_entry)
866
+ result = self.amlg.CloneEntity(
867
+ handle_buf, clone_handle_buf, file_path_buf, file_type_buf, persist,
868
+ json_file_params_buf, write_log_buf, print_log_buf)
869
+ self._log_reply(result)
870
+
871
+ del handle_buf
872
+ del clone_handle_buf
873
+ del file_path_buf
874
+ del file_type_buf
875
+ del json_file_params_buf
876
+ del write_log_buf
877
+ del print_log_buf
878
+ self.gc()
879
+
880
+ return result
881
+
882
+ def store_entity(
883
+ self,
884
+ handle: str,
885
+ file_path: str,
886
+ *,
887
+ file_type: str = "",
888
+ persist: bool = False,
889
+ json_file_params: str = "",
890
+ ):
891
+ """
892
+ Store entity to the file type specified within file_path.
893
+
894
+ Parameters
895
+ ----------
896
+ handle : str
897
+ The handle of the amalgam entity.
898
+ file_path : str
899
+ The path of the file name to load.
900
+ file_type : str, default ""
901
+ If set to a nonempty string, will represent the type of file to load.
902
+ persist : bool, default False
903
+ If set to true, all transactions that update the entity will also be
904
+ written to the files.
905
+ json_file_params : str, default ""
906
+ Either empty string or a string of json specifying a set of key-value pairs
907
+ which are parameters specific to the file type. See Amalgam documentation
908
+ for details of allowed parameters.
909
+ """
910
+ self.amlg.StoreEntity.argtypes = [
911
+ c_char_p, c_char_p, c_char_p, c_bool, c_char_p]
912
+ handle_buf = self.str_to_char_p(handle)
913
+ file_path_buf = self.str_to_char_p(file_path)
914
+ file_type_buf = self.str_to_char_p(file_type)
915
+ json_file_params_buf = self.str_to_char_p(json_file_params)
916
+
917
+ store_command_log_entry = (
918
+ f"STORE_ENTITY \"{self.escape_double_quotes(handle)}\" "
919
+ f"\"{self.escape_double_quotes(file_path)}\" "
920
+ f"\"{self.escape_double_quotes(file_type)}\" {str(persist).lower()} "
921
+ f"{json_lib.dumps(json_file_params)} "
922
+ )
923
+ self._log_execution(store_command_log_entry)
924
+ self.amlg.StoreEntity(
925
+ handle_buf, file_path_buf, file_type_buf, persist, json_file_params_buf)
926
+ self._log_reply(None)
927
+
928
+ del handle_buf
929
+ del file_path_buf
930
+ del file_type_buf
931
+ del json_file_params_buf
932
+ self.gc()
933
+
934
+ def destroy_entity(
935
+ self,
936
+ handle: str
937
+ ):
938
+ """
939
+ Destroys an entity.
940
+
941
+ Parameters
942
+ ----------
943
+ handle : str
944
+ The handle of the amalgam entity.
945
+ """
946
+ self.amlg.DestroyEntity.argtypes = [c_char_p]
947
+ handle_buf = self.str_to_char_p(handle)
948
+
949
+ self._log_execution(f"DESTROY_ENTITY \"{self.escape_double_quotes(handle)}\"")
950
+ self.amlg.DestroyEntity(handle_buf)
951
+ self._log_reply(None)
952
+
953
+ del handle_buf
954
+ self.gc()
955
+
956
+ def set_random_seed(
957
+ self,
958
+ handle: str,
959
+ rand_seed: str
960
+ ) -> bool:
961
+ """
962
+ Set entity's random seed.
963
+
964
+ Parameters
965
+ ----------
966
+ handle : str
967
+ The handle of the amalgam entity.
968
+ rand_seed : str
969
+ A string representing the random seed to set.
970
+
971
+ Returns
972
+ -------
973
+ bool
974
+ True if the set was successful, false if not.
975
+ """
976
+ self.amlg.SetRandomSeed.argtypes = [c_char_p, c_char_p]
977
+ self.amlg.SetRandomSeed.restype = c_bool
978
+
979
+ handle_buf = self.str_to_char_p(handle)
980
+ rand_seed_buf = self.str_to_char_p(rand_seed)
981
+
982
+ self._log_execution(f'SET_RANDOM_SEED "{self.escape_double_quotes(handle)}"'
983
+ f'"{self.escape_double_quotes(rand_seed)}"')
984
+ result = self.amlg.SetRandomSeed(handle_buf, rand_seed)
985
+ self._log_reply(None)
986
+
987
+ del handle_buf
988
+ del rand_seed_buf
989
+ self.gc()
990
+ return result
991
+
992
+ def get_entities(self) -> list[str]:
993
+ """
994
+ Get loaded top level entities.
995
+
996
+ Returns
997
+ -------
998
+ list of str
999
+ The list of entity handles.
1000
+ """
1001
+ self.amlg.GetEntities.argtypes = [POINTER(c_uint64)]
1002
+ self.amlg.GetEntities.restype = POINTER(c_char_p)
1003
+ num_entities = c_uint64()
1004
+ entities = self.amlg.GetEntities(byref(num_entities))
1005
+ result = [entities[i].decode() for i in range(num_entities.value)]
1006
+
1007
+ del entities
1008
+ del num_entities
1009
+ self.gc()
1010
+
1011
+ return result
1012
+
1013
+ def execute_entity_json(
1014
+ self,
1015
+ handle: str,
1016
+ label: str,
1017
+ json: str | bytes
1018
+ ) -> bytes:
1019
+ """
1020
+ Execute a label with parameters provided in json format.
1021
+
1022
+ Parameters
1023
+ ----------
1024
+ handle : str
1025
+ The handle of the amalgam entity.
1026
+ label : str
1027
+ The label to execute.
1028
+ json : str or bytes
1029
+ A json representation of parameters for the label to be executed.
1030
+
1031
+ Returns
1032
+ -------
1033
+ bytes
1034
+ A byte-encoded json representation of the response.
1035
+ """
1036
+ self.amlg.ExecuteEntityJsonPtr.restype = POINTER(c_char)
1037
+ self.amlg.ExecuteEntityJsonPtr.argtypes = [
1038
+ c_char_p, c_char_p, c_char_p]
1039
+ handle_buf = self.str_to_char_p(handle)
1040
+ label_buf = self.str_to_char_p(label)
1041
+ json_buf = self.str_to_char_p(json)
1042
+
1043
+ self._log_time("EXECUTION START")
1044
+ self._log_execution((
1045
+ "EXECUTE_ENTITY_JSON "
1046
+ f"\"{self.escape_double_quotes(handle)}\" "
1047
+ f"\"{self.escape_double_quotes(label)}\" "
1048
+ f"{json}"
1049
+ ))
1050
+ result = self.char_p_to_bytes(self.amlg.ExecuteEntityJsonPtr(
1051
+ handle_buf, label_buf, json_buf))
1052
+ self._log_time("EXECUTION STOP")
1053
+ self._log_reply(result)
1054
+
1055
+ del handle_buf
1056
+ del label_buf
1057
+ del json_buf
1058
+
1059
+ return result
1060
+
1061
+ def get_version_string(self) -> bytes:
1062
+ """
1063
+ Get the version string of the amalgam dynamic library.
1064
+
1065
+ Returns
1066
+ -------
1067
+ bytes
1068
+ A version byte-encoded string with semver.
1069
+ """
1070
+ self.amlg.GetVersionString.restype = POINTER(c_char)
1071
+ amlg_version = self.char_p_to_bytes(self.amlg.GetVersionString())
1072
+ self._log_comment(f"call to amlg.GetVersionString() - returned: "
1073
+ f"{amlg_version}\n")
1074
+ return amlg_version
1075
+
1076
+ def get_concurrency_type_string(self) -> bytes:
1077
+ """
1078
+ Get the concurrency type string of the amalgam dynamic library.
1079
+
1080
+ Returns
1081
+ -------
1082
+ bytes
1083
+ A byte-encoded string with library concurrency type.
1084
+ Ex. b'MultiThreaded'
1085
+ """
1086
+ self.amlg.GetConcurrencyTypeString.restype = POINTER(c_char)
1087
+ amlg_concurrency_type = self.char_p_to_bytes(self.amlg.GetConcurrencyTypeString())
1088
+ self._log_comment(
1089
+ f"call to amlg.GetConcurrencyTypeString() - returned: "
1090
+ f"{amlg_concurrency_type}\n")
1091
+ return amlg_concurrency_type
1092
+
1093
+ @staticmethod
1094
+ def escape_double_quotes(s: str) -> str:
1095
+ """
1096
+ Get the string with backslashes preceding contained double quotes.
1097
+
1098
+ Parameters
1099
+ ----------
1100
+ s : str
1101
+ The input string.
1102
+
1103
+ Returns
1104
+ -------
1105
+ str
1106
+ The modified version of s with escaped double quotes.
1107
+ """
1108
+ return s.replace('"', '\\"')