cryptodb 2.4.3__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.
nedb/redis_compat.py ADDED
@@ -0,0 +1,516 @@
1
+ """
2
+ nedb.redis_compat — Redis command compatibility adapter.
3
+
4
+ Maps the Redis command surface deterministically to NEDB primitives. No Redis
5
+ or hiredis code is used or required — Redis commands are a familiar entry point;
6
+ NEDB executes everything natively using its append-only log, MVCC, and relations.
7
+
8
+ Usage::
9
+
10
+ from nedb import NEDB
11
+ from nedb.redis_compat import RedisCompat
12
+
13
+ db = NEDB("./data")
14
+ r = RedisCompat(db)
15
+
16
+ r.execute("SET", "mykey", "hello") # → "OK"
17
+ r.execute("GET", "mykey") # → "hello"
18
+ r.execute("HSET", "user:1", "name", "Ada", "age", "31")
19
+ r.execute("HGETALL", "user:1") # → {"name": "Ada", "age": "31"}
20
+ r.execute("SADD", "tags", "python", "rust")
21
+ r.execute("SMEMBERS", "tags") # → {"python", "rust"}
22
+
23
+ Key → NEDB mapping
24
+ ──────────────────
25
+ Strings (SET/GET) → collection "_kv", id = key, doc = {"_v": value}
26
+ Hashes (HSET/…) → collection = key, id = field, doc = {"_v": value}
27
+ Sets (SADD/…) → relation edges from "_set:<key>" to "_smember:<key>:<member>"
28
+ Lists (LPUSH/…) → collection "_list:<key>", auto-seq id, doc = {"_v", "_seq"}
29
+
30
+ Unsupported commands (todo):
31
+ EXPIRE, TTL, PEXPIRE, PTTL, PERSIST — no built-in TTL mechanism
32
+ SUBSCRIBE, PUBLISH, UNSUBSCRIBE — no pub-sub layer
33
+ MULTI, EXEC, DISCARD, WATCH — no transaction isolation
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ import time
39
+ import uuid
40
+ from typing import Any, Dict, List, Optional, Set
41
+
42
+
43
+ def _safe(name: str) -> str:
44
+ """Encode a Redis key into a NQL-safe NEDB collection name.
45
+ Characters outside [A-Za-z0-9_] are replaced with __XX__ (hex).
46
+ This is deterministic and collision-free — 'user:1' → 'user__3a__1'.
47
+ """
48
+ return re.sub(r"[^A-Za-z0-9_]", lambda m: f"__{ord(m.group()):02x}__", name)
49
+
50
+
51
+ _KV_COLL = "_kv"
52
+ _LIST_PREFIX = "_list_"
53
+ _SET_NODE_PREFIX = "_set_"
54
+ _SET_MEMBER_PREFIX = "_smember_"
55
+
56
+ UNSUPPORTED = {
57
+ "EXPIRE", "EXPIREAT", "EXPIRETIME", "PEXPIRE", "PEXPIREAT",
58
+ "TTL", "PTTL", "PERSIST",
59
+ "SUBSCRIBE", "UNSUBSCRIBE", "PSUBSCRIBE", "PUNSUBSCRIBE",
60
+ "PUBLISH", "PUBSUB",
61
+ "MULTI", "EXEC", "DISCARD", "WATCH", "UNWATCH",
62
+ "WAIT", "OBJECT", "DEBUG", "MONITOR", "SLOWLOG",
63
+ "CLUSTER", "REPLICAOF", "SLAVEOF", "BGSAVE", "BGREWRITEAOF",
64
+ "LASTSAVE", "SAVE", "RESTORE", "DUMP", "MIGRATE", "MOVE",
65
+ "SORT", "EVAL", "EVALSHA", "SCRIPT",
66
+ "GEORADIUSBYMEMBER", "GEOADD", "GEODIST", "GEOPOS",
67
+ "XADD", "XREAD", "XLEN", "XRANGE",
68
+ "BF.ADD", "CF.ADD",
69
+ }
70
+
71
+ _TODO_REASON = {
72
+ "EXPIRE": "TTL/expiry is on the NEDB roadmap. Track: github.com/Eth-Interchained/nedb/issues.",
73
+ "TTL": "TTL is on the NEDB roadmap.",
74
+ "PTTL": "TTL is on the NEDB roadmap.",
75
+ "SUBSCRIBE": "Pub-sub is on the NEDB roadmap.",
76
+ "PUBLISH": "Pub-sub is on the NEDB roadmap.",
77
+ "MULTI": "Transactions (MULTI/EXEC) are on the NEDB roadmap.",
78
+ "EXEC": "Transactions (MULTI/EXEC) are on the NEDB roadmap.",
79
+ "DISCARD": "Transactions are on the NEDB roadmap.",
80
+ }
81
+
82
+
83
+ class RedisUnsupportedError(Exception):
84
+ """Raised when a Redis command is not yet implemented in NEDB."""
85
+
86
+
87
+ class RedisError(Exception):
88
+ """Raised on a Redis-compatible argument or usage error."""
89
+
90
+
91
+ class RedisCompat:
92
+ """
93
+ Redis-compatible command interface over a NEDB database.
94
+
95
+ Each command is executed transactionally (append-only log) with NEDB's
96
+ replay-protection. Pass ``client`` to scope nonce counters per service.
97
+ """
98
+
99
+ def __init__(self, db: Any, client: str = "redis-compat"):
100
+ self._db = db
101
+ self._client = client
102
+
103
+ # ── dispatch ──────────────────────────────────────────────────────────────
104
+
105
+ def execute(self, command: str, *args: Any) -> Any:
106
+ """
107
+ Execute a Redis command and return the result.
108
+
109
+ Arguments mirror the Redis protocol — all string, no type coercion
110
+ except what Redis itself performs (INCR coerces to int, HSET takes
111
+ alternating field/value pairs, etc.).
112
+ """
113
+ cmd = command.upper()
114
+
115
+ if cmd in UNSUPPORTED:
116
+ reason = _TODO_REASON.get(cmd, "Not yet implemented in NEDB.")
117
+ raise RedisUnsupportedError(
118
+ f"{cmd} is not supported. {reason}"
119
+ )
120
+
121
+ # ── String commands ────────────────────────────────────────────────
122
+ if cmd == "PING":
123
+ return "PONG" if not args else args[0]
124
+ if cmd == "SET":
125
+ return self._set(*args)
126
+ if cmd == "GET":
127
+ return self._get(*args)
128
+ if cmd == "GETDEL":
129
+ return self._getdel(*args)
130
+ if cmd == "SETNX":
131
+ return self._setnx(*args)
132
+ if cmd == "MSET":
133
+ return self._mset(*args)
134
+ if cmd == "MGET":
135
+ return self._mget(*args)
136
+ if cmd == "DEL":
137
+ return self._del(*args)
138
+ if cmd == "UNLINK":
139
+ return self._del(*args) # same semantics for our purposes
140
+ if cmd == "EXISTS":
141
+ return self._exists(*args)
142
+ if cmd == "INCR":
143
+ return self._incrby(args[0], 1)
144
+ if cmd == "INCRBY":
145
+ return self._incrby(args[0], int(args[1]))
146
+ if cmd == "DECR":
147
+ return self._incrby(args[0], -1)
148
+ if cmd == "DECRBY":
149
+ return self._incrby(args[0], -int(args[1]))
150
+ if cmd == "APPEND":
151
+ return self._append(*args)
152
+ if cmd == "STRLEN":
153
+ v = self._get(args[0])
154
+ return len(str(v)) if v is not None else 0
155
+ if cmd == "TYPE":
156
+ return self._type(*args)
157
+ if cmd == "RENAME":
158
+ return self._rename(*args)
159
+ if cmd == "KEYS":
160
+ return self._keys(args[0] if args else "*")
161
+ if cmd == "DBSIZE":
162
+ return len(self._keys("*"))
163
+ if cmd == "FLUSHDB":
164
+ return self._flushdb()
165
+
166
+ # ── Hash commands ──────────────────────────────────────────────────
167
+ if cmd == "HSET":
168
+ return self._hset(*args)
169
+ if cmd == "HMSET":
170
+ self._hset(*args); return "OK"
171
+ if cmd == "HSETNX":
172
+ return self._hsetnx(*args)
173
+ if cmd == "HGET":
174
+ return self._hget(*args)
175
+ if cmd == "HMGET":
176
+ return self._hmget(*args)
177
+ if cmd == "HGETALL":
178
+ return self._hgetall(*args)
179
+ if cmd == "HDEL":
180
+ return self._hdel(*args)
181
+ if cmd == "HEXISTS":
182
+ return self._hexists(*args)
183
+ if cmd == "HKEYS":
184
+ return self._hkeys(*args)
185
+ if cmd == "HVALS":
186
+ return self._hvals(*args)
187
+ if cmd == "HLEN":
188
+ return self._hlen(*args)
189
+ if cmd == "HINCRBY":
190
+ return self._hincrby(*args)
191
+
192
+ # ── Set commands ───────────────────────────────────────────────────
193
+ if cmd == "SADD":
194
+ return self._sadd(*args)
195
+ if cmd == "SMEMBERS":
196
+ return self._smembers(*args)
197
+ if cmd == "SISMEMBER":
198
+ return self._sismember(*args)
199
+ if cmd == "SREM":
200
+ return self._srem(*args)
201
+ if cmd == "SCARD":
202
+ return self._scard(*args)
203
+ if cmd == "SUNION":
204
+ result: Set[str] = set()
205
+ for k in args:
206
+ result |= self._smembers(k)
207
+ return result
208
+ if cmd == "SINTER":
209
+ sets = [self._smembers(k) for k in args]
210
+ return sets[0].intersection(*sets[1:]) if sets else set()
211
+ if cmd == "SDIFF":
212
+ sets = [self._smembers(k) for k in args]
213
+ return sets[0].difference(*sets[1:]) if sets else set()
214
+
215
+ # ── List commands ──────────────────────────────────────────────────
216
+ if cmd == "LPUSH":
217
+ return self._lpush(*args)
218
+ if cmd == "RPUSH":
219
+ return self._rpush(*args)
220
+ if cmd == "LRANGE":
221
+ return self._lrange(*args)
222
+ if cmd == "LLEN":
223
+ return self._llen(*args)
224
+ if cmd == "LINDEX":
225
+ return self._lindex(*args)
226
+ if cmd == "LSET":
227
+ return self._lset(*args)
228
+ if cmd == "LPOP":
229
+ return self._lpop(*args)
230
+ if cmd == "RPOP":
231
+ return self._rpop(*args)
232
+
233
+ raise RedisError(f"Unknown command: {command!r}")
234
+
235
+ # ── Internal helpers ──────────────────────────────────────────────────────
236
+
237
+ def _put(self, coll: str, row_id: str, doc: Dict[str, Any], idem: Optional[str] = None) -> Any:
238
+ return self._db.put(coll, row_id, doc, client=self._client, idem=idem)
239
+
240
+ def _get_raw(self, coll: str, row_id: str) -> Optional[Dict[str, Any]]:
241
+ return self._db.get(coll, row_id)
242
+
243
+ def _del_raw(self, coll: str, row_id: str) -> None:
244
+ self._db.delete(coll, row_id, client=self._client)
245
+
246
+ # ── String ops ────────────────────────────────────────────────────────────
247
+
248
+ def _set(self, key: str, value: Any, *opts: Any) -> str:
249
+ flags = {str(o).upper() for o in opts}
250
+ if "NX" in flags:
251
+ return self._setnx(key, value)
252
+ if "XX" in flags:
253
+ if self._get_raw(_KV_COLL, key) is None:
254
+ return None # type: ignore[return-value]
255
+ self._put(_KV_COLL, str(key), {"_v": str(value)})
256
+ return "OK"
257
+
258
+ def _get(self, key: str) -> Optional[str]:
259
+ doc = self._get_raw(_KV_COLL, str(key))
260
+ return str(doc["_v"]) if doc and "_v" in doc else None
261
+
262
+ def _getdel(self, key: str) -> Optional[str]:
263
+ val = self._get(key)
264
+ if val is not None:
265
+ self._del_raw(_KV_COLL, str(key))
266
+ return val
267
+
268
+ def _setnx(self, key: str, value: Any) -> int:
269
+ if self._get_raw(_KV_COLL, str(key)) is not None:
270
+ return 0
271
+ self._put(_KV_COLL, str(key), {"_v": str(value)})
272
+ return 1
273
+
274
+ def _mset(self, *pairs: Any) -> str:
275
+ if len(pairs) % 2:
276
+ raise RedisError("MSET requires an even number of arguments (key value ...)")
277
+ for i in range(0, len(pairs), 2):
278
+ self._set(pairs[i], pairs[i + 1])
279
+ return "OK"
280
+
281
+ def _mget(self, *keys: Any) -> List[Optional[str]]:
282
+ return [self._get(k) for k in keys]
283
+
284
+ def _del(self, *keys: Any) -> int:
285
+ count = 0
286
+ for k in keys:
287
+ doc = self._get_raw(_KV_COLL, str(k))
288
+ if doc is not None:
289
+ self._del_raw(_KV_COLL, str(k))
290
+ count += 1
291
+ return count
292
+
293
+ def _exists(self, *keys: Any) -> int:
294
+ return sum(1 for k in keys if self._get_raw(_KV_COLL, str(k)) is not None)
295
+
296
+ def _incrby(self, key: str, delta: int) -> int:
297
+ doc = self._get_raw(_KV_COLL, str(key))
298
+ current = int(doc["_v"]) if doc and "_v" in doc else 0
299
+ new_val = current + delta
300
+ self._put(_KV_COLL, str(key), {"_v": str(new_val)})
301
+ return new_val
302
+
303
+ def _append(self, key: str, value: str) -> int:
304
+ existing = self._get(key) or ""
305
+ combined = existing + str(value)
306
+ self._put(_KV_COLL, str(key), {"_v": combined})
307
+ return len(combined)
308
+
309
+ def _type(self, key: str) -> str:
310
+ if self._get_raw(_KV_COLL, str(key)) is not None:
311
+ return "string"
312
+ if self._hlen(str(key)) > 0:
313
+ return "hash"
314
+ if self._scard(str(key)) > 0:
315
+ return "set"
316
+ if self._llen(str(key)) > 0:
317
+ return "list"
318
+ return "none"
319
+
320
+ def _rename(self, src: str, dst: str) -> str:
321
+ val = self._get(src)
322
+ if val is None:
323
+ raise RedisError(f"ERR no such key: {src!r}")
324
+ self._set(dst, val)
325
+ self._del_raw(_KV_COLL, str(src))
326
+ return "OK"
327
+
328
+ def _keys(self, pattern: str = "*") -> List[str]:
329
+ import fnmatch
330
+ rows = self._db.query(f"FROM {_KV_COLL}")
331
+ all_keys = [r.get("_id", "") for r in rows]
332
+ if pattern == "*":
333
+ return all_keys
334
+ return [k for k in all_keys if fnmatch.fnmatch(k, pattern)]
335
+
336
+ def _flushdb(self) -> str:
337
+ for key in self._keys("*"):
338
+ self._del_raw(_KV_COLL, key)
339
+ return "OK"
340
+
341
+ # ── Hash ops (collection = hash name; id = field) ─────────────────────────
342
+
343
+ def _hset(self, name: str, *pairs: Any) -> int:
344
+ if len(pairs) % 2:
345
+ raise RedisError("HSET requires alternating field value pairs")
346
+ coll = _safe(str(name))
347
+ created = 0
348
+ for i in range(0, len(pairs), 2):
349
+ field, value = str(pairs[i]), pairs[i + 1]
350
+ existed = self._get_raw(coll, field) is not None
351
+ self._put(coll, field, {"_v": str(value)})
352
+ if not existed:
353
+ created += 1
354
+ return created
355
+
356
+ def _hsetnx(self, name: str, field: str, value: Any) -> int:
357
+ coll = _safe(str(name))
358
+ if self._get_raw(coll, str(field)) is not None:
359
+ return 0
360
+ self._put(coll, str(field), {"_v": str(value)})
361
+ return 1
362
+
363
+ def _hget(self, name: str, field: str) -> Optional[str]:
364
+ doc = self._get_raw(_safe(str(name)), str(field))
365
+ return str(doc["_v"]) if doc and "_v" in doc else None
366
+
367
+ def _hmget(self, name: str, *fields: Any) -> List[Optional[str]]:
368
+ return [self._hget(str(name), str(f)) for f in fields]
369
+
370
+ def _hgetall(self, name: str) -> Dict[str, str]:
371
+ rows = self._db.query(f"FROM {_safe(str(name))}")
372
+ return {r["_id"]: str(r["_v"]) for r in rows if "_v" in r}
373
+
374
+ def _hdel(self, name: str, *fields: Any) -> int:
375
+ coll = _safe(str(name))
376
+ count = 0
377
+ for f in fields:
378
+ if self._get_raw(coll, str(f)) is not None:
379
+ self._del_raw(coll, str(f))
380
+ count += 1
381
+ return count
382
+
383
+ def _hexists(self, name: str, field: str) -> int:
384
+ return 1 if self._get_raw(_safe(str(name)), str(field)) is not None else 0
385
+
386
+ def _hkeys(self, name: str) -> List[str]:
387
+ return [r["_id"] for r in self._db.query(f"FROM {_safe(str(name))}") if "_v" in r]
388
+
389
+ def _hvals(self, name: str) -> List[str]:
390
+ return [str(r["_v"]) for r in self._db.query(f"FROM {_safe(str(name))}") if "_v" in r]
391
+
392
+ def _hlen(self, name: str) -> int:
393
+ return len(self._hkeys(name))
394
+
395
+ def _hincrby(self, name: str, field: str, delta: Any) -> int:
396
+ coll = _safe(str(name))
397
+ doc = self._get_raw(coll, str(field))
398
+ current = int(doc["_v"]) if doc and "_v" in doc else 0
399
+ new_val = current + int(delta)
400
+ self._put(coll, str(field), {"_v": str(new_val)})
401
+ return new_val
402
+
403
+ # ── Set ops (using NEDB relations) ────────────────────────────────────────
404
+
405
+ def _set_node(self, key: str) -> str:
406
+ return f"{_SET_NODE_PREFIX}{_safe(key)}"
407
+
408
+ def _set_member_node(self, key: str, member: str) -> str:
409
+ return f"{_SET_MEMBER_PREFIX}{_safe(key)}__{_safe(member)}"
410
+
411
+ def _sadd(self, key: str, *members: Any) -> int:
412
+ count = 0
413
+ node = self._set_node(str(key))
414
+ existing = set(self._smembers(str(key)))
415
+ for m in members:
416
+ ms = str(m)
417
+ if ms not in existing:
418
+ target = self._set_member_node(str(key), ms)
419
+ self._db.link(node, "member", target, client=self._client)
420
+ existing.add(ms)
421
+ count += 1
422
+ return count
423
+
424
+ def _smembers(self, key: str) -> Set[str]:
425
+ node = self._set_node(str(key))
426
+ prefix = f"{_SET_MEMBER_PREFIX}{_safe(str(key))}__"
427
+ neighbors = self._db.neighbors(node, "member")
428
+ return {nb[len(prefix):] for nb in neighbors if nb.startswith(prefix)}
429
+
430
+ def _sismember(self, key: str, member: str) -> int:
431
+ return 1 if str(member) in self._smembers(str(key)) else 0
432
+
433
+ def _srem(self, key: str, *members: Any) -> int:
434
+ count = 0
435
+ node = self._set_node(str(key))
436
+ existing = self._smembers(str(key))
437
+ for m in members:
438
+ ms = str(m)
439
+ if ms in existing:
440
+ target = self._set_member_node(str(key), ms)
441
+ self._db.unlink(node, "member", target, client=self._client)
442
+ count += 1
443
+ return count
444
+
445
+ def _scard(self, key: str) -> int:
446
+ return len(self._smembers(str(key)))
447
+
448
+ # ── List ops (append-only; id = zero-padded timestamp for order) ──────────
449
+
450
+ def _list_coll(self, key: str) -> str:
451
+ return f"{_LIST_PREFIX}{_safe(key)}"
452
+
453
+ def _lpush(self, key: str, *values: Any) -> int:
454
+ coll = self._list_coll(str(key))
455
+ for v in reversed(values):
456
+ seq_id = f"L-{time.monotonic_ns():020d}-{uuid.uuid4().hex[:6]}"
457
+ self._put(coll, seq_id, {"_v": str(v), "_side": "L", "_ts": time.monotonic_ns()})
458
+ return self._llen(str(key))
459
+
460
+ def _rpush(self, key: str, *values: Any) -> int:
461
+ coll = self._list_coll(str(key))
462
+ for v in values:
463
+ seq_id = f"R-{time.monotonic_ns():020d}-{uuid.uuid4().hex[:6]}"
464
+ self._put(coll, seq_id, {"_v": str(v), "_side": "R", "_ts": time.monotonic_ns()})
465
+ return self._llen(str(key))
466
+
467
+ def _list_rows(self, key: str) -> List[Dict[str, Any]]:
468
+ coll = self._list_coll(str(key))
469
+ rows = self._db.query(f"FROM {coll}")
470
+ return sorted(rows, key=lambda r: (r.get("_side", "R"), r.get("_ts", 0)))
471
+
472
+ def _lrange(self, key: str, start: Any, stop: Any) -> List[str]:
473
+ rows = self._list_rows(str(key))
474
+ vals = [str(r["_v"]) for r in rows if "_v" in r]
475
+ start, stop = int(start), int(stop)
476
+ if stop == -1:
477
+ stop = len(vals) - 1
478
+ return vals[start:stop + 1]
479
+
480
+ def _llen(self, key: str) -> int:
481
+ return len(self._list_rows(str(key)))
482
+
483
+ def _lindex(self, key: str, index: Any) -> Optional[str]:
484
+ vals = self._lrange(str(key), 0, -1)
485
+ i = int(index)
486
+ return vals[i] if 0 <= i < len(vals) else (vals[i] if -len(vals) <= i < 0 else None)
487
+
488
+ def _lset(self, key: str, index: Any, value: Any) -> str:
489
+ rows = self._list_rows(str(key))
490
+ i = int(index)
491
+ if i < 0:
492
+ i = len(rows) + i
493
+ if i < 0 or i >= len(rows):
494
+ raise RedisError("ERR index out of range")
495
+ coll = self._list_coll(str(key))
496
+ row = rows[i]
497
+ self._put(coll, row["_id"], {**row, "_v": str(value)})
498
+ return "OK"
499
+
500
+ def _lpop(self, key: str) -> Optional[str]:
501
+ rows = self._list_rows(str(key))
502
+ if not rows:
503
+ return None
504
+ r = rows[0]
505
+ coll = self._list_coll(str(key))
506
+ self._del_raw(coll, r["_id"])
507
+ return str(r.get("_v", ""))
508
+
509
+ def _rpop(self, key: str) -> Optional[str]:
510
+ rows = self._list_rows(str(key))
511
+ if not rows:
512
+ return None
513
+ r = rows[-1]
514
+ coll = self._list_coll(str(key))
515
+ self._del_raw(coll, r["_id"])
516
+ return str(r.get("_v", ""))
nedb/relations.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ nedb.relations — first-class, time-travel-aware relations (the graph layer).
3
+
4
+ Relations are stored as adjacency lists for O(1) traversal. Each edge records the
5
+ seq at which it was added and (optionally) removed, so relation queries can also be
6
+ asked "AS OF" any past sequence — the graph time-travels just like the records do.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Dict, List, Optional, Tuple
11
+
12
+
13
+ class Relations:
14
+ def __init__(self) -> None:
15
+ # (frm, rel) -> list of [to, added_seq, removed_seq|None]
16
+ self._adj: Dict[Tuple[str, str], List[list]] = {}
17
+ # (to, rel) -> list of [frm, added_seq, removed_seq|None] (reverse index)
18
+ self._radj: Dict[Tuple[str, str], List[list]] = {}
19
+
20
+ def link(self, frm: str, rel: str, to: str, seq: int) -> None:
21
+ for e in self._adj.get((frm, rel), []):
22
+ if e[0] == to and e[2] is None:
23
+ return # already linked
24
+ self._adj.setdefault((frm, rel), []).append([to, seq, None])
25
+ self._radj.setdefault((to, rel), []).append([frm, seq, None])
26
+
27
+ def unlink(self, frm: str, rel: str, to: str, seq: int) -> None:
28
+ for e in self._adj.get((frm, rel), []):
29
+ if e[0] == to and e[2] is None:
30
+ e[2] = seq
31
+ for e in self._radj.get((to, rel), []):
32
+ if e[0] == frm and e[2] is None:
33
+ e[2] = seq
34
+
35
+ @staticmethod
36
+ def _live(edges, as_of):
37
+ out = []
38
+ for node, added, removed in edges:
39
+ if as_of is None:
40
+ if removed is None:
41
+ out.append(node)
42
+ else:
43
+ if added <= as_of and (removed is None or removed > as_of):
44
+ out.append(node)
45
+ return out
46
+
47
+ def neighbors(self, frm: str, rel: str, as_of: Optional[int] = None) -> List[str]:
48
+ return self._live(self._adj.get((frm, rel), []), as_of)
49
+
50
+ def inbound(self, to: str, rel: str, as_of: Optional[int] = None) -> List[str]:
51
+ return self._live(self._radj.get((to, rel), []), as_of)