zlmdb 25.10.1__cp314-cp314-win_amd64.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 zlmdb might be problematic. Click here for more details.

Files changed (86) hide show
  1. flatbuffers/__init__.py +19 -0
  2. flatbuffers/_version.py +17 -0
  3. flatbuffers/builder.py +776 -0
  4. flatbuffers/compat.py +86 -0
  5. flatbuffers/encode.py +42 -0
  6. flatbuffers/flexbuffers.py +1527 -0
  7. flatbuffers/number_types.py +181 -0
  8. flatbuffers/packer.py +42 -0
  9. flatbuffers/reflection/AdvancedFeatures.py +10 -0
  10. flatbuffers/reflection/BaseType.py +24 -0
  11. flatbuffers/reflection/Enum.py +169 -0
  12. flatbuffers/reflection/EnumVal.py +96 -0
  13. flatbuffers/reflection/Field.py +208 -0
  14. flatbuffers/reflection/KeyValue.py +56 -0
  15. flatbuffers/reflection/Object.py +175 -0
  16. flatbuffers/reflection/RPCCall.py +131 -0
  17. flatbuffers/reflection/Schema.py +206 -0
  18. flatbuffers/reflection/SchemaFile.py +77 -0
  19. flatbuffers/reflection/Service.py +145 -0
  20. flatbuffers/reflection/Type.py +98 -0
  21. flatbuffers/reflection/__init__.py +0 -0
  22. flatbuffers/table.py +129 -0
  23. flatbuffers/util.py +43 -0
  24. zlmdb/__init__.py +312 -0
  25. zlmdb/_database.py +990 -0
  26. zlmdb/_errors.py +31 -0
  27. zlmdb/_meta.py +27 -0
  28. zlmdb/_pmap.py +1667 -0
  29. zlmdb/_schema.py +137 -0
  30. zlmdb/_transaction.py +181 -0
  31. zlmdb/_types.py +1596 -0
  32. zlmdb/_version.py +27 -0
  33. zlmdb/cli.py +41 -0
  34. zlmdb/flatbuffers/__init__.py +5 -0
  35. zlmdb/flatbuffers/reflection/AdvancedFeatures.py +10 -0
  36. zlmdb/flatbuffers/reflection/BaseType.py +25 -0
  37. zlmdb/flatbuffers/reflection/Enum.py +252 -0
  38. zlmdb/flatbuffers/reflection/EnumVal.py +144 -0
  39. zlmdb/flatbuffers/reflection/Field.py +325 -0
  40. zlmdb/flatbuffers/reflection/KeyValue.py +84 -0
  41. zlmdb/flatbuffers/reflection/Object.py +260 -0
  42. zlmdb/flatbuffers/reflection/RPCCall.py +195 -0
  43. zlmdb/flatbuffers/reflection/Schema.py +301 -0
  44. zlmdb/flatbuffers/reflection/SchemaFile.py +112 -0
  45. zlmdb/flatbuffers/reflection/Service.py +213 -0
  46. zlmdb/flatbuffers/reflection/Type.py +148 -0
  47. zlmdb/flatbuffers/reflection/__init__.py +0 -0
  48. zlmdb/flatbuffers/reflection.fbs +152 -0
  49. zlmdb/lmdb/__init__.py +37 -0
  50. zlmdb/lmdb/__main__.py +25 -0
  51. zlmdb/lmdb/_config.py +10 -0
  52. zlmdb/lmdb/cffi.py +2606 -0
  53. zlmdb/lmdb/tool.py +670 -0
  54. zlmdb/tests/lmdb/__init__.py +0 -0
  55. zlmdb/tests/lmdb/address_book.py +287 -0
  56. zlmdb/tests/lmdb/crash_test.py +339 -0
  57. zlmdb/tests/lmdb/cursor_test.py +333 -0
  58. zlmdb/tests/lmdb/env_test.py +919 -0
  59. zlmdb/tests/lmdb/getmulti_test.py +92 -0
  60. zlmdb/tests/lmdb/iteration_test.py +258 -0
  61. zlmdb/tests/lmdb/package_test.py +70 -0
  62. zlmdb/tests/lmdb/test_lmdb.py +188 -0
  63. zlmdb/tests/lmdb/testlib.py +185 -0
  64. zlmdb/tests/lmdb/tool_test.py +60 -0
  65. zlmdb/tests/lmdb/txn_test.py +575 -0
  66. zlmdb/tests/orm/MNodeLog.py +853 -0
  67. zlmdb/tests/orm/__init__.py +0 -0
  68. zlmdb/tests/orm/_schema_fbs.py +215 -0
  69. zlmdb/tests/orm/_schema_mnode_log.py +1201 -0
  70. zlmdb/tests/orm/_schema_py2.py +250 -0
  71. zlmdb/tests/orm/_schema_py3.py +307 -0
  72. zlmdb/tests/orm/_test_flatbuffers.py +144 -0
  73. zlmdb/tests/orm/_test_serialization.py +144 -0
  74. zlmdb/tests/orm/test_basic.py +217 -0
  75. zlmdb/tests/orm/test_etcd.py +275 -0
  76. zlmdb/tests/orm/test_pmap_indexes.py +466 -0
  77. zlmdb/tests/orm/test_pmap_types.py +90 -0
  78. zlmdb/tests/orm/test_pmaps.py +295 -0
  79. zlmdb/tests/orm/test_select.py +619 -0
  80. zlmdb-25.10.1.dist-info/METADATA +264 -0
  81. zlmdb-25.10.1.dist-info/RECORD +86 -0
  82. zlmdb-25.10.1.dist-info/WHEEL +5 -0
  83. zlmdb-25.10.1.dist-info/entry_points.txt +2 -0
  84. zlmdb-25.10.1.dist-info/licenses/LICENSE +137 -0
  85. zlmdb-25.10.1.dist-info/licenses/NOTICE +41 -0
  86. zlmdb-25.10.1.dist-info/top_level.txt +2 -0
zlmdb/_database.py ADDED
@@ -0,0 +1,990 @@
1
+ #############################################################################
2
+ #
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) typedef int GmbH
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in
15
+ # all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ # THE SOFTWARE.
24
+ #
25
+ ###############################################################################
26
+
27
+ import os
28
+ import shutil
29
+ import tempfile
30
+ import uuid
31
+ import pprint
32
+ import struct
33
+ import inspect
34
+ import time
35
+ from typing import Dict, Any, Tuple, List, Optional, Callable, Type
36
+
37
+ import zlmdb.lmdb as lmdb
38
+ import yaml
39
+ import cbor2
40
+
41
+ from zlmdb._transaction import Transaction, TransactionStats
42
+ from zlmdb import _pmap
43
+ from zlmdb._pmap import MapStringJson, MapStringCbor, MapUuidJson, MapUuidCbor
44
+
45
+ import txaio
46
+
47
+ try:
48
+ from twisted.python.reflect import qual
49
+ except ImportError:
50
+
51
+ def qual(clazz: type) -> str: # type: ignore[misc]
52
+ return clazz.__name__
53
+
54
+
55
+ KV_TYPE_TO_CLASS = {
56
+ "string-json": (MapStringJson, lambda x: x, lambda x: x),
57
+ "string-cbor": (MapStringCbor, lambda x: x, lambda x: x),
58
+ "uuid-json": (MapUuidJson, lambda x: x, lambda x: x),
59
+ "uuid-cbor": (MapUuidCbor, lambda x: x, lambda x: x),
60
+ }
61
+
62
+ _LMDB_MYPID_ENVS: Dict[str, Tuple["Database", int]] = {}
63
+
64
+
65
+ class ConfigurationElement(object):
66
+ """
67
+ Internal zLMDB configuration element base type.
68
+ """
69
+
70
+ __slots__ = (
71
+ "_oid",
72
+ "_name",
73
+ "_description",
74
+ "_tags",
75
+ )
76
+
77
+ def __init__(
78
+ self,
79
+ oid: Optional[uuid.UUID] = None,
80
+ name: Optional[str] = None,
81
+ description: Optional[str] = None,
82
+ tags: Optional[List[str]] = None,
83
+ ):
84
+ self._oid = oid
85
+ self._name = name
86
+ self._description = description
87
+ self._tags = tags
88
+
89
+ def __eq__(self, other: Any) -> bool:
90
+ if not isinstance(other, self.__class__):
91
+ return False
92
+ if other.oid != self.oid:
93
+ return False
94
+ if other.name != self.name:
95
+ return False
96
+ if other.description != self.description:
97
+ return False
98
+ if (self.tags and not other.tags) or (not self.tags and other.tags):
99
+ return False
100
+ if other.tags and self.tags:
101
+ if set(other.tags) ^ set(self.tags):
102
+ return False
103
+ return True
104
+
105
+ def __ne__(self, other: Any) -> bool:
106
+ return not self.__eq__(other)
107
+
108
+ @property
109
+ def oid(self) -> Optional[uuid.UUID]:
110
+ return self._oid
111
+
112
+ @property
113
+ def name(self) -> Optional[str]:
114
+ return self._name
115
+
116
+ @property
117
+ def description(self) -> Optional[str]:
118
+ return self._description
119
+
120
+ @property
121
+ def tags(self) -> Optional[List[str]]:
122
+ return self._tags
123
+
124
+ def __str__(self) -> str:
125
+ return pprint.pformat(self.marshal())
126
+
127
+ def marshal(self) -> Dict[str, Any]:
128
+ value: Dict[str, Any] = {
129
+ "oid": str(self._oid),
130
+ "name": self._name,
131
+ }
132
+ if self.description:
133
+ value["description"] = self._description
134
+ if self.tags:
135
+ value["tags"] = self._tags
136
+ return value
137
+
138
+ @staticmethod
139
+ def parse(value: Dict[str, Any]) -> "ConfigurationElement":
140
+ assert type(value) == dict
141
+ oid = value.get("oid", None)
142
+ if oid:
143
+ oid = uuid.UUID(oid)
144
+ obj = ConfigurationElement(
145
+ oid=oid,
146
+ name=value.get("name", None),
147
+ description=value.get("description", None),
148
+ tags=value.get("tags", None),
149
+ )
150
+ return obj
151
+
152
+
153
+ class Slot(ConfigurationElement):
154
+ """
155
+ Internal zLMDB database slot configuration element.
156
+ """
157
+
158
+ __slots__ = (
159
+ "_slot",
160
+ "_creator",
161
+ )
162
+
163
+ def __init__(
164
+ self,
165
+ oid: Optional[uuid.UUID] = None,
166
+ name: Optional[str] = None,
167
+ description: Optional[str] = None,
168
+ tags: Optional[List[str]] = None,
169
+ slot: Optional[int] = None,
170
+ creator: Optional[str] = None,
171
+ ):
172
+ ConfigurationElement.__init__(
173
+ self, oid=oid, name=name, description=description, tags=tags
174
+ )
175
+ self._slot = slot
176
+ self._creator = creator
177
+
178
+ @property
179
+ def creator(self) -> Optional[str]:
180
+ return self._creator
181
+
182
+ @property
183
+ def slot(self) -> Optional[int]:
184
+ return self._slot
185
+
186
+ def __str__(self) -> str:
187
+ return pprint.pformat(self.marshal())
188
+
189
+ def marshal(self) -> Dict[str, Any]:
190
+ obj = ConfigurationElement.marshal(self)
191
+ obj.update(
192
+ {
193
+ "creator": self._creator,
194
+ "slot": self._slot,
195
+ }
196
+ )
197
+ return obj
198
+
199
+ @staticmethod
200
+ def parse(data: Dict[str, Any]) -> "Slot":
201
+ assert type(data) == dict
202
+
203
+ obj = ConfigurationElement.parse(data)
204
+
205
+ slot = data.get("slot", None)
206
+ creator = data.get("creator", None)
207
+
208
+ drvd_obj = Slot(
209
+ oid=obj.oid,
210
+ name=obj.name,
211
+ description=obj.description,
212
+ tags=obj.tags,
213
+ slot=slot,
214
+ creator=creator,
215
+ )
216
+ return drvd_obj
217
+
218
+
219
+ class Schema(object):
220
+ def __init__(self, meta, slots, slots_byname):
221
+ self._meta = meta
222
+ self._slots = slots
223
+ self._slots_byname = slots_byname
224
+
225
+ def __str__(self):
226
+ return pprint.pformat(self._meta)
227
+
228
+ def __getitem__(self, name):
229
+ assert type(name) == str
230
+
231
+ if name not in self._slots_byname:
232
+ raise IndexError('no slot with name "{}"'.format(name))
233
+
234
+ return self._slots[self._slots_byname[name]]
235
+
236
+ def __setitem__(self, name, value):
237
+ raise NotImplementedError("schema is read-only")
238
+
239
+ def __delitem__(self, name):
240
+ raise NotImplementedError("schema is read-only")
241
+
242
+ def __len__(self):
243
+ return len(self._slots_byname)
244
+
245
+ def __iter__(self):
246
+ raise Exception("not implemented")
247
+
248
+ @staticmethod
249
+ def parse(filename, klassmap=KV_TYPE_TO_CLASS):
250
+ with open(filename) as f:
251
+ _meta = yaml.safe_load(f.read())
252
+
253
+ meta = {}
254
+ slots = {}
255
+ slots_byname = {}
256
+
257
+ for slot in _meta.get("slots", []):
258
+ _index = slot.get("index", None)
259
+ assert type(_index) == int and _index >= 100 and _index < 65536
260
+ assert _index not in slots
261
+
262
+ _name = slot.get("name", None)
263
+ assert type(_name) == str
264
+ assert _name not in slots_byname
265
+
266
+ _key = slot.get("key", None)
267
+ assert _key in ["string", "uuid"]
268
+
269
+ _value = slot.get("value", None)
270
+ assert _value in ["json", "cbor"]
271
+
272
+ _schema = slot.get("schema", None)
273
+ assert _schema is None or type(_schema) == str
274
+
275
+ _description = slot.get("description", None)
276
+ assert _description is None or type(_description) == str
277
+
278
+ if _schema:
279
+ _kv_type = "{}-{}-{}".format(_key, _value, _schema)
280
+ else:
281
+ _kv_type = "{}-{}".format(_key, _value)
282
+
283
+ _kv_klass, _marshal, _unmarshal = klassmap.get(
284
+ _kv_type, (None, None, None)
285
+ )
286
+
287
+ assert _kv_klass
288
+ assert _marshal
289
+ assert _unmarshal
290
+
291
+ meta[_index] = {
292
+ "index": _index,
293
+ "name": _name,
294
+ "key": _key,
295
+ "value": _value,
296
+ "impl": _kv_klass.__name__ if _kv_klass else None,
297
+ "description": _description,
298
+ }
299
+ slots[_index] = _kv_klass(
300
+ _index, marshal=_marshal, unmarshal=_unmarshal
301
+ )
302
+ slots_byname[_name] = _index
303
+
304
+ return Schema(meta, slots, slots_byname)
305
+
306
+
307
+ class Database(object):
308
+ """
309
+ ZLMDB database access.
310
+
311
+ Objects of this class are generally "light-weight" (cheap to create and
312
+ destroy), but do manage internal resource such as file descriptors.
313
+
314
+ To manage these resources in a robust way, this class implements
315
+ the Python context manager interface.
316
+ """
317
+
318
+ __slots__ = (
319
+ "log",
320
+ "_is_temp",
321
+ "_tempdir",
322
+ "_dbpath",
323
+ "_maxsize",
324
+ "_readonly",
325
+ "_lock",
326
+ "_sync",
327
+ "_create",
328
+ "_open_now",
329
+ "_writemap",
330
+ "_context",
331
+ "_slots",
332
+ "_slots_by_index",
333
+ "_env",
334
+ )
335
+
336
+ def __init__(
337
+ self,
338
+ dbpath: Optional[str] = None,
339
+ maxsize: int = 10485760,
340
+ readonly: bool = False,
341
+ lock: bool = True,
342
+ sync: bool = True,
343
+ create: bool = True,
344
+ open_now: bool = True,
345
+ writemap: bool = False,
346
+ context: Any = None,
347
+ log: Optional[txaio.interfaces.ILogger] = None,
348
+ ):
349
+ """
350
+
351
+ :param dbpath: LMDB database path: a directory with (at least) 2 files, a ``data.mdb`` and a ``lock.mdb``.
352
+ If no database exists at the given path, create a new one.
353
+ :param maxsize: Database size limit in bytes, with a default of 1MB.
354
+ :param readonly: Open database read-only. When ``True``, deny any modifying database operations.
355
+ Note that the LMDB lock file (``lock.mdb``) still needs to be written (by readers also),
356
+ and hence at the filesystem level, a LMDB database directory must be writable.
357
+ :param sync: Open database with sync on commit.
358
+ :param create: Automatically create database if it does not yet exist.
359
+ :param open_now: Open the database immediately (within this constructor).
360
+ :param writemap: Use direct write to mmap'ed database rather than regular file IO writes. Be careful when
361
+ using any storage other than locally attached filesystem/drive.
362
+ :param context: Optional context within which this database instance is created.
363
+ :param log: Log object to use for logging from this class.
364
+ """
365
+ self._context = context
366
+
367
+ if log:
368
+ self.log = log
369
+ else:
370
+ if not txaio._explicit_framework:
371
+ txaio.use_asyncio()
372
+ self.log = txaio.make_logger()
373
+
374
+ if dbpath:
375
+ self._is_temp = False
376
+ self._tempdir = None
377
+ self._dbpath = dbpath
378
+ else:
379
+ self._is_temp = True
380
+ self._tempdir = tempfile.TemporaryDirectory()
381
+ self._dbpath = self._tempdir.name
382
+
383
+ self._maxsize = maxsize
384
+ self._readonly = readonly
385
+ self._lock = lock
386
+ self._sync = sync
387
+ self._create = create
388
+ self._open_now = open_now
389
+ self._writemap = writemap
390
+ self._context = context
391
+
392
+ self._slots: Optional[Dict[uuid.UUID, Slot]] = None
393
+ self._slots_by_index: Optional[Dict[uuid.UUID, int]] = None
394
+
395
+ # in a context manager environment we initialize with LMDB handle
396
+ # when we enter the actual temporary, managed context ..
397
+ self._env: Optional[lmdb.Environment] = None
398
+
399
+ # in a direct run environment, we immediately open LMDB
400
+ if self._open_now:
401
+ self.__enter__()
402
+
403
+ def __enter__(self):
404
+ """
405
+ Enter database runtime context and open the underlying LMDB database environment.
406
+
407
+ .. note::
408
+
409
+ Enter the runtime context related to this object. The with statement will bind this method’s
410
+ return value to the target(s) specified in the as clause of the statement, if any.
411
+ [Source](https://docs.python.org/3/reference/datamodel.html#object.__enter__)
412
+
413
+ .. note::
414
+
415
+ A context manager is an object that defines the runtime context to be established when
416
+ executing a with statement. The context manager handles the entry into, and the exit from,
417
+ the desired runtime context for the execution of the block of code. Context managers are
418
+ normally invoked using the with statement (described in section The with statement), but
419
+ can also be used by directly invoking their methods."
420
+ [Source](https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers)
421
+
422
+ :return: This database instance (in open state).
423
+ """
424
+ if not self._env:
425
+ # protect against opening the same database file multiple times within the same process:
426
+ # "It is a serious error to have open (multiple times) the same LMDB file in
427
+ # the same process at the same time. Failure to heed this may lead to data
428
+ # corruption and interpreter crash."
429
+ # https://lmdb.readthedocs.io/en/release/#environment-class
430
+
431
+ if not self._is_temp:
432
+ if self._dbpath in _LMDB_MYPID_ENVS:
433
+ other_obj, other_pid = _LMDB_MYPID_ENVS[self._dbpath]
434
+ raise RuntimeError(
435
+ 'tried to open same dbpath "{}" twice within same process: cannot open database '
436
+ "for {} (PID {}, Context {}), already opened in {} (PID {}, Context {})".format(
437
+ self._dbpath,
438
+ self,
439
+ os.getpid(),
440
+ self.context,
441
+ other_obj,
442
+ other_pid,
443
+ other_obj.context,
444
+ )
445
+ )
446
+ _LMDB_MYPID_ENVS[self._dbpath] = self, os.getpid()
447
+
448
+ # handle lmdb.LockError: mdb_txn_begin: Resource temporarily unavailable
449
+ # "The environment was locked by another process."
450
+ # https://lmdb.readthedocs.io/en/release/#lmdb.LockError
451
+
452
+ # count number of retries
453
+ retries = 0
454
+ # delay (in seconds) before retrying
455
+ retry_delay = 0
456
+ while True:
457
+ try:
458
+ # https://lmdb.readthedocs.io/en/release/#lmdb.Environment
459
+ # https://lmdb.readthedocs.io/en/release/#writemap-mode
460
+ # map_size: Maximum size database may grow to; used to size the memory mapping.
461
+ # lock=True is needed for concurrent access, even when only by readers (because of space mgmt)
462
+ self._env = lmdb.open(
463
+ self._dbpath,
464
+ map_size=self._maxsize,
465
+ create=self._create,
466
+ readonly=self._readonly,
467
+ sync=self._sync,
468
+ subdir=True,
469
+ lock=self._lock,
470
+ writemap=self._writemap,
471
+ )
472
+
473
+ # ok, good: we've got a LMDB env
474
+ break
475
+
476
+ # see https://github.com/crossbario/zlmdb/issues/53
477
+ except lmdb.LockError as e:
478
+ retries += 1
479
+ if retries >= 3:
480
+ # give up and signal to user code
481
+ raise RuntimeError(
482
+ "cannot open LMDB environment (giving up "
483
+ "after {} retries): {}".format(retries, e)
484
+ )
485
+
486
+ # use synchronous (!) sleep (1st time is sleep(0), which releases execution of this process to OS)
487
+ time.sleep(retry_delay)
488
+
489
+ # increase sleep time by 10ms _next_ time. that is, for our 3 attempts
490
+ # the delays are: 0ms, 10ms, 20ms
491
+ retry_delay += 0.01
492
+
493
+ return self
494
+
495
+ def __exit__(self, exc_type, exc_value, traceback):
496
+ """
497
+ Exit runtime context and close the underlying LMDB database environment.
498
+
499
+ .. note::
500
+
501
+ Exit the runtime context related to this object. The parameters describe the exception that
502
+ caused the context to be exited. If the context was exited without an exception, all three
503
+ arguments will be None.
504
+ [Source](https://docs.python.org/3/reference/datamodel.html#object.__exit__).
505
+
506
+ :param exc_type:
507
+ :param exc_value:
508
+ :param traceback:
509
+ :return:
510
+ """
511
+ if self._env:
512
+ self._env.close()
513
+ self._env = None
514
+ if not self._is_temp and self._dbpath in _LMDB_MYPID_ENVS:
515
+ del _LMDB_MYPID_ENVS[self._dbpath]
516
+
517
+ @staticmethod
518
+ def open(
519
+ dbpath: Optional[str] = None,
520
+ maxsize: int = 10485760,
521
+ readonly: bool = False,
522
+ lock: bool = True,
523
+ sync: bool = True,
524
+ create: bool = True,
525
+ open_now: bool = True,
526
+ writemap: bool = False,
527
+ context: Any = None,
528
+ log: Optional[txaio.interfaces.ILogger] = None,
529
+ ) -> "Database":
530
+ if dbpath is not None and dbpath in _LMDB_MYPID_ENVS:
531
+ db, _ = _LMDB_MYPID_ENVS[dbpath]
532
+ print(
533
+ '{}: reusing database instance for path "{}" in new context {} already opened from (first) context {}'.format(
534
+ Database.open, dbpath, context, db.context
535
+ )
536
+ )
537
+ else:
538
+ db = Database(
539
+ dbpath=dbpath,
540
+ maxsize=maxsize,
541
+ readonly=readonly,
542
+ lock=lock,
543
+ sync=sync,
544
+ create=create,
545
+ open_now=open_now,
546
+ writemap=writemap,
547
+ context=context,
548
+ log=log,
549
+ )
550
+ print(
551
+ '{}: creating new database instance for path "{}" in context {}'.format(
552
+ Database.open, dbpath, context
553
+ )
554
+ )
555
+ return db
556
+
557
+ @property
558
+ def context(self):
559
+ """
560
+
561
+ :return:
562
+ """
563
+ return self._context
564
+
565
+ @property
566
+ def dbpath(self) -> Optional[str]:
567
+ """
568
+
569
+ :return:
570
+ """
571
+ return self._dbpath
572
+
573
+ @property
574
+ def maxsize(self) -> int:
575
+ """
576
+
577
+ :return:
578
+ """
579
+ return self._maxsize
580
+
581
+ @property
582
+ def is_sync(self) -> bool:
583
+ """
584
+
585
+ :return:
586
+ """
587
+ return self._sync
588
+
589
+ @property
590
+ def is_readonly(self) -> bool:
591
+ """
592
+
593
+ :return:
594
+ """
595
+ return self._readonly
596
+
597
+ @property
598
+ def is_writemap(self) -> bool:
599
+ """
600
+
601
+ :return:
602
+ """
603
+ return self._writemap
604
+
605
+ @property
606
+ def is_open(self) -> bool:
607
+ """
608
+
609
+ :return:
610
+ """
611
+ return self._env is not None
612
+
613
+ @staticmethod
614
+ def scratch(dbpath: str):
615
+ """
616
+
617
+ :param dbpath:
618
+ :return:
619
+ """
620
+ if os.path.exists(dbpath):
621
+ if os.path.isdir(dbpath):
622
+ shutil.rmtree(dbpath)
623
+ else:
624
+ os.remove(dbpath)
625
+
626
+ def begin(
627
+ self,
628
+ write: bool = False,
629
+ buffers: bool = False,
630
+ stats: Optional[TransactionStats] = None,
631
+ ) -> Transaction:
632
+ """
633
+
634
+ :param write:
635
+ :param buffers:
636
+ :param stats:
637
+ :return:
638
+ """
639
+ assert self._env is not None
640
+
641
+ if write and self._readonly:
642
+ raise Exception("database is read-only")
643
+
644
+ txn = Transaction(db=self, write=write, buffers=buffers, stats=stats)
645
+ return txn
646
+
647
+ def sync(self, force: bool = False):
648
+ """
649
+
650
+ :param force:
651
+ :return:
652
+ """
653
+ assert self._env is not None
654
+
655
+ self._env.sync(force=force)
656
+
657
+ def config(self) -> Dict[str, Any]:
658
+ """
659
+
660
+ :return:
661
+ """
662
+ res = {
663
+ "is_temp": self._is_temp,
664
+ "dbpath": self._dbpath,
665
+ "maxsize": self._maxsize,
666
+ "readonly": self._readonly,
667
+ "lock": self._lock,
668
+ "sync": self._sync,
669
+ "create": self._create,
670
+ "open_now": self._open_now,
671
+ "writemap": self._writemap,
672
+ "context": str(self._context) if self._context else None,
673
+ }
674
+ return res
675
+
676
+ def stats(self, include_slots: bool = False) -> Dict[str, Any]:
677
+ """
678
+
679
+ :param include_slots:
680
+ :return:
681
+ """
682
+ assert self._env is not None
683
+
684
+ current_size = os.path.getsize(os.path.join(self._dbpath, "data.mdb"))
685
+
686
+ # psize Size of a database page in bytes.
687
+ # depth Height of the B-tree.
688
+ # branch_pages Number of internal (non-leaf) pages.
689
+ # leaf_pages Number of leaf pages.
690
+ # overflow_pages Number of overflow pages.
691
+ # entries Number of data items.
692
+ stats = self._env.stat()
693
+ pages = stats["leaf_pages"] + stats["overflow_pages"] + stats["branch_pages"]
694
+ used = stats["psize"] * pages
695
+
696
+ self._cache_slots()
697
+ res: Dict[str, Any] = {
698
+ "num_slots": len(self._slots) if self._slots else 0,
699
+ "current_size": current_size,
700
+ "max_size": self._maxsize,
701
+ "page_size": stats["psize"],
702
+ "pages": pages,
703
+ "used": used,
704
+ "free": 1.0 - float(used) / float(self._maxsize),
705
+ "read_only": self._readonly,
706
+ "sync_enabled": self._sync,
707
+ }
708
+ res.update(stats)
709
+
710
+ # map_addr Address of database map in RAM.
711
+ # map_size Size of database map in RAM.
712
+ # last_pgno ID of last used page.
713
+ # last_txnid ID of last committed transaction.
714
+ # max_readers Number of reader slots allocated in the lock file. Equivalent to the value of
715
+ # maxreaders= specified by the first process opening the Environment.
716
+ # num_readers Maximum number of reader slots in simultaneous use since the lock file was initialized.
717
+ res.update(self._env.info())
718
+
719
+ if include_slots:
720
+ slots = self._get_slots()
721
+ res["slots"] = []
722
+ with self.begin() as txn:
723
+ for slot_id in slots:
724
+ slot = slots[slot_id]
725
+ pmap = _pmap.PersistentMap(slot.slot)
726
+ res["slots"].append(
727
+ {
728
+ "oid": str(slot_id),
729
+ "slot": slot.slot,
730
+ "name": slot.name,
731
+ "description": slot.description,
732
+ "records": pmap.count(txn),
733
+ }
734
+ )
735
+
736
+ return res
737
+
738
+ def _cache_slots(self):
739
+ """
740
+
741
+ :return:
742
+ """
743
+ slots = {}
744
+ slots_by_index = {}
745
+
746
+ with self.begin() as txn:
747
+ from_key = struct.pack(">H", 0)
748
+ to_key = struct.pack(">H", 1)
749
+
750
+ cursor = txn._txn.cursor()
751
+ found = cursor.set_range(from_key)
752
+ while found:
753
+ _key = cursor.key()
754
+ if _key >= to_key:
755
+ break
756
+
757
+ if len(_key) >= 4:
758
+ # key = struct.unpack('>H', _key[0:2])
759
+ slot_index = struct.unpack(">H", _key[2:4])[0]
760
+ slot = Slot.parse(cbor2.loads(cursor.value()))
761
+ assert slot.slot == slot_index
762
+ slots[slot.oid] = slot
763
+ slots_by_index[slot.oid] = slot_index
764
+
765
+ found = cursor.next()
766
+
767
+ self._slots = slots
768
+ self._slots_by_index = slots_by_index
769
+
770
+ def _get_slots(self, cached=True) -> Dict[uuid.UUID, Slot]:
771
+ """
772
+
773
+ :param cached:
774
+ :return:
775
+ """
776
+ if self._slots is None or not cached:
777
+ self._cache_slots()
778
+ assert self._slots
779
+ return self._slots
780
+
781
+ def _get_free_slot(self) -> int:
782
+ """
783
+
784
+ :return:
785
+ """
786
+ if self._slots_by_index is None:
787
+ self._cache_slots()
788
+ assert self._slots_by_index is not None
789
+ slot_indexes = sorted(self._slots_by_index.values())
790
+ if len(slot_indexes) > 0:
791
+ return slot_indexes[-1] + 1
792
+ else:
793
+ return 1
794
+
795
+ def _set_slot(self, slot_index: int, slot: Optional[Slot]):
796
+ """
797
+
798
+ :param slot_index:
799
+ :param slot:
800
+ :return:
801
+ """
802
+ assert type(slot_index) == int
803
+ assert 0 < slot_index < 65536
804
+ assert slot is None or isinstance(slot, Slot)
805
+
806
+ if self._slots is None:
807
+ self._cache_slots()
808
+ assert self._slots is not None
809
+ assert self._slots_by_index is not None
810
+
811
+ key = b"\0\0" + struct.pack(">H", slot_index)
812
+ if slot:
813
+ assert slot_index == slot.slot
814
+ assert slot.oid
815
+
816
+ data = cbor2.dumps(slot.marshal())
817
+ with self.begin(write=True) as txn:
818
+ txn._txn.put(key, data)
819
+ self._slots[slot.oid] = slot
820
+ self._slots_by_index[slot.oid] = slot_index
821
+
822
+ self.log.debug(
823
+ "Wrote metadata for table <{oid}> to slot {slot_index:03d}",
824
+ oid=slot.oid,
825
+ slot_index=slot_index,
826
+ )
827
+ else:
828
+ with self.begin(write=True) as txn:
829
+ result = txn.get(key)
830
+ if result:
831
+ txn._txn.delete(key)
832
+ slot = Slot.parse(cbor2.loads(result))
833
+ if slot.oid in self._slots:
834
+ del self._slots[slot.oid]
835
+ if slot.oid in self._slots_by_index:
836
+ del self._slots_by_index[slot.oid]
837
+
838
+ self.log.debug(
839
+ "Deleted metadata for table <{oid}> from slot {slot_index:03d}",
840
+ oid=slot.oid,
841
+ slot_index=slot_index,
842
+ )
843
+
844
+ def attach_table(self, klass: Type[_pmap.PersistentMap]):
845
+ """
846
+
847
+ :param klass:
848
+ :return:
849
+ """
850
+ if not inspect.isclass(klass):
851
+ raise TypeError(
852
+ "cannot attach object {} as database table: a subclass of zlmdb.PersistentMap is required".format(
853
+ klass
854
+ )
855
+ )
856
+
857
+ name = qual(klass)
858
+
859
+ if not issubclass(klass, _pmap.PersistentMap):
860
+ raise TypeError(
861
+ "cannot attach object of class {} as a database table: a subclass of zlmdb.PersistentMap is required".format(
862
+ name
863
+ )
864
+ )
865
+
866
+ if not hasattr(klass, "_zlmdb_oid") or not klass._zlmdb_oid:
867
+ raise TypeError("{} is not decorated as table slot".format(klass))
868
+
869
+ description = klass.__doc__.strip() if klass.__doc__ else None
870
+
871
+ if self._slots is None:
872
+ self._cache_slots()
873
+
874
+ pmap = self._attach_slot(
875
+ klass._zlmdb_oid,
876
+ klass,
877
+ marshal=klass._zlmdb_marshal,
878
+ parse=klass._zlmdb_parse,
879
+ build=klass._zlmdb_build,
880
+ cast=klass._zlmdb_cast,
881
+ compress=klass._zlmdb_compress,
882
+ create=True,
883
+ name=name,
884
+ description=description,
885
+ )
886
+ return pmap
887
+
888
+ def _attach_slot(
889
+ self,
890
+ oid: uuid.UUID,
891
+ klass: Type[_pmap.PersistentMap],
892
+ marshal: Optional[Callable] = None,
893
+ parse: Optional[Callable] = None,
894
+ build: Optional[Callable] = None,
895
+ cast: Optional[Callable] = None,
896
+ compress: Optional[int] = None,
897
+ create: bool = True,
898
+ name: Optional[str] = None,
899
+ description: Optional[str] = None,
900
+ ):
901
+ """
902
+
903
+ :param oid:
904
+ :param klass:
905
+ :param marshal:
906
+ :param parse:
907
+ :param build:
908
+ :param cast:
909
+ :param compress:
910
+ :param create:
911
+ :param name:
912
+ :param description:
913
+ :return:
914
+ """
915
+ assert isinstance(oid, uuid.UUID)
916
+ assert issubclass(klass, _pmap.PersistentMap)
917
+
918
+ assert marshal is None or callable(marshal)
919
+ assert parse is None or callable(parse)
920
+
921
+ assert build is None or callable(build)
922
+ assert cast is None or callable(cast)
923
+
924
+ # either marshal+parse (for CBOR/JSON) OR build+cast (for Flatbuffers) OR all unset
925
+ assert (
926
+ (not marshal and not parse and not build and not cast)
927
+ or (not marshal and not parse and build and cast)
928
+ or (marshal and parse and not build and not cast)
929
+ )
930
+
931
+ assert compress is None or compress in [
932
+ _pmap.PersistentMap.COMPRESS_ZLIB,
933
+ _pmap.PersistentMap.COMPRESS_SNAPPY,
934
+ ]
935
+ assert type(create) == bool
936
+ assert name is None or type(name) == str
937
+ assert description is None or type(description) == str
938
+
939
+ assert self._slots_by_index is not None
940
+
941
+ if oid not in self._slots_by_index:
942
+ self.log.debug(
943
+ "No slot found in database for DB table <{oid}>: <{name}>",
944
+ name=name,
945
+ oid=oid,
946
+ )
947
+ if create:
948
+ slot_index = self._get_free_slot()
949
+ slot = Slot(
950
+ oid=oid,
951
+ creator="unknown",
952
+ slot=slot_index,
953
+ name=name,
954
+ description=description,
955
+ )
956
+ self._set_slot(slot_index, slot)
957
+ self.log.info(
958
+ "Allocated new slot {slot_index:03d} for database table <{oid}>: {name}",
959
+ slot_index=slot_index,
960
+ oid=oid,
961
+ name=name,
962
+ )
963
+ else:
964
+ raise RuntimeError(
965
+ 'No slot found in database for DB table <{}>: "{}"'.format(
966
+ oid, name
967
+ )
968
+ )
969
+ else:
970
+ slot_index = self._slots_by_index[oid]
971
+ # pmap = _pmap.PersistentMap(slot_index)
972
+ # with self.begin() as txn:
973
+ # records = pmap.count(txn)
974
+ self.log.debug(
975
+ "Database table <{name}> attached [oid=<{oid}>, slot=<{slot_index:03d}>]",
976
+ name=name,
977
+ oid=oid,
978
+ slot_index=slot_index,
979
+ )
980
+
981
+ if marshal:
982
+ slot_pmap = klass(
983
+ slot_index, marshal=marshal, unmarshal=parse, compress=compress
984
+ ) # type: ignore
985
+ elif build:
986
+ slot_pmap = klass(slot_index, build=build, cast=cast, compress=compress) # type: ignore
987
+ else:
988
+ slot_pmap = klass(slot_index, compress=compress)
989
+
990
+ return slot_pmap