gremlinpython 4.0.0b2__py3-none-any.whl → 4.0.0.dev1__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.
@@ -26,7 +26,7 @@ class Graph(object):
26
26
  self.edges = {}
27
27
 
28
28
  def __repr__(self):
29
- return "graph[]"
29
+ return "graph[vertices:" + str(len(self.vertices)) + " edges:" + str(len(self.edges)) + "]"
30
30
 
31
31
 
32
32
  class Element(object):
@@ -64,19 +64,37 @@ class Element(object):
64
64
 
65
65
 
66
66
  class Vertex(Element):
67
- def __init__(self, id, label="vertex", properties=None):
68
- Element.__init__(self, id, label, properties)
67
+ def __init__(self, id, label="vertex", properties=None, labels=None):
68
+ if labels is not None:
69
+ self._labels = set(labels)
70
+ Element.__init__(self, id, next(iter(labels)) if labels else "", properties)
71
+ else:
72
+ self._labels = {label} if label else {"vertex"}
73
+ Element.__init__(self, id, label or "vertex", properties)
74
+
75
+ @property
76
+ def labels(self):
77
+ return frozenset(self._labels)
69
78
 
70
79
  def __repr__(self):
71
80
  return "v[" + str(self.id) + "]"
72
81
 
73
82
 
74
83
  class Edge(Element):
75
- def __init__(self, id, outV, label, inV, properties=None):
76
- Element.__init__(self, id, label, properties)
84
+ def __init__(self, id, outV, label, inV, properties=None, labels=None):
85
+ if labels is not None:
86
+ self._labels = set(labels)
87
+ Element.__init__(self, id, next(iter(labels)) if labels else "", properties)
88
+ else:
89
+ self._labels = {label} if label else {"edge"}
90
+ Element.__init__(self, id, label or "edge", properties)
77
91
  self.outV = outV
78
92
  self.inV = inV
79
93
 
94
+ @property
95
+ def labels(self):
96
+ return frozenset(self._labels)
97
+
80
98
  def __repr__(self):
81
99
  return "e[" + str(self.id) + "][" + str(self.outV.id) + "-" + self.label + "->" + str(self.inV.id) + "]"
82
100
 
@@ -141,3 +159,430 @@ class Path(object):
141
159
 
142
160
  def __len__(self):
143
161
  return len(self.objects)
162
+
163
+
164
+ class CompositePDT(object):
165
+ def __init__(self, name, fields):
166
+ if not name:
167
+ raise ValueError("name cannot be null or empty")
168
+ self._name = name
169
+ self._fields = dict(fields) if fields else {}
170
+ if any(not isinstance(k, str) for k in self._fields):
171
+ raise TypeError("CompositePDT field keys must be strings")
172
+
173
+ @property
174
+ def name(self):
175
+ return self._name
176
+
177
+ @property
178
+ def fields(self):
179
+ return self._fields
180
+
181
+ def __eq__(self, other):
182
+ return isinstance(other, CompositePDT) and self._name == other._name and self._fields == other._fields
183
+
184
+ def __hash__(self):
185
+ try:
186
+ return hash((self._name, frozenset(self._fields.items())))
187
+ except TypeError:
188
+ return hash(self._name)
189
+
190
+ def __repr__(self):
191
+ return f"pdt[{self._name}]{self._fields}"
192
+
193
+
194
+ class PrimitivePDT(object):
195
+ """An immutable primitive provider-defined type consisting of a name and an opaque string value."""
196
+
197
+ def __init__(self, name, value):
198
+ if not name:
199
+ raise ValueError("name cannot be null or empty")
200
+ if value is None:
201
+ raise ValueError("value cannot be null")
202
+ self._name = name
203
+ self._value = value
204
+
205
+ @property
206
+ def name(self):
207
+ return self._name
208
+
209
+ @property
210
+ def value(self):
211
+ return self._value
212
+
213
+ def __eq__(self, other):
214
+ return isinstance(other, PrimitivePDT) and self._name == other._name and self._value == other._value
215
+
216
+ def __hash__(self):
217
+ return hash((self._name, self._value))
218
+
219
+ def __repr__(self):
220
+ return f"pdt[{self._name}]({self._value})"
221
+
222
+
223
+ class PDTRegistry(object):
224
+ def __init__(self):
225
+ self._composite_adapters_by_name = {}
226
+ self._composite_adapters_by_class = {}
227
+ self._primitive_adapters_by_name = {}
228
+ self._primitive_adapters_by_class = {}
229
+
230
+ def register(self, type_name, deserialize_fn, serialize_fn=None, target_class=None):
231
+ self._composite_adapters_by_name[type_name] = {
232
+ 'deserialize': deserialize_fn,
233
+ 'serialize': serialize_fn,
234
+ 'target_class': target_class
235
+ }
236
+ if target_class is not None:
237
+ self._composite_adapters_by_class[target_class] = {
238
+ 'type_name': type_name,
239
+ 'serialize': serialize_fn,
240
+ }
241
+
242
+ def register_primitive(self, type_name, from_value, to_value=None, target_class=None):
243
+ """Register a primitive PDT adapter.
244
+
245
+ Args:
246
+ type_name: The PDT type name string.
247
+ from_value: Callable(str) -> object for deserialization.
248
+ to_value: Callable(object) -> str for serialization (optional).
249
+ target_class: The Python class this adapter produces (optional).
250
+ """
251
+ self._primitive_adapters_by_name[type_name] = {
252
+ 'from_value': from_value,
253
+ 'to_value': to_value,
254
+ 'target_class': target_class
255
+ }
256
+ if target_class is not None:
257
+ self._primitive_adapters_by_class[target_class] = {
258
+ 'type_name': type_name,
259
+ 'to_value': to_value,
260
+ }
261
+
262
+ @classmethod
263
+ def create(cls):
264
+ """Create a registry populated by entry_points discovery.
265
+
266
+ Providers register adapters via pyproject.toml:
267
+ [project.entry-points."tinkerpop.pdt"]
268
+ my_types = "my_package:register_pdt_types"
269
+
270
+ Each entry point should be a callable that accepts a registry and registers adapters.
271
+ """
272
+ import sys
273
+ registry = cls()
274
+ if sys.version_info >= (3, 10):
275
+ from importlib.metadata import entry_points
276
+ eps = entry_points(group='tinkerpop.pdt')
277
+ else:
278
+ from importlib.metadata import entry_points
279
+ all_eps = entry_points()
280
+ eps = all_eps.get('tinkerpop.pdt', [])
281
+
282
+ for ep in eps:
283
+ try:
284
+ factory = ep.load()
285
+ factory(registry)
286
+ except Exception as e:
287
+ import logging
288
+ logging.getLogger(__name__).warning(
289
+ f"Failed to load PDT adapter from entry point '{ep.name}': {e}")
290
+ return registry
291
+
292
+ def hydrate(self, pdt):
293
+ """Attempt to hydrate a CompositePDT. Returns typed object or raw PDT."""
294
+ if not isinstance(pdt, CompositePDT):
295
+ return pdt
296
+
297
+ # Always recurse into fields to hydrate nested registered PDTs.
298
+ changed = False
299
+ hydrated_fields = {}
300
+ for k, v in pdt.fields.items():
301
+ if isinstance(v, CompositePDT):
302
+ h = self.hydrate(v)
303
+ if h is not v:
304
+ changed = True
305
+ hydrated_fields[k] = h
306
+ elif isinstance(v, PrimitivePDT):
307
+ h = self.hydrate_primitive(v)
308
+ if h is not v:
309
+ changed = True
310
+ hydrated_fields[k] = h
311
+ else:
312
+ hydrated_fields[k] = v
313
+
314
+ adapter = self._composite_adapters_by_name.get(pdt.name)
315
+ if adapter is None:
316
+ return CompositePDT(pdt.name, hydrated_fields) if changed else pdt
317
+ try:
318
+ return adapter['deserialize'](hydrated_fields)
319
+ except Exception as e:
320
+ import logging
321
+ logging.getLogger(__name__).warning(f"PDT hydration failed for '{pdt.name}': {e}")
322
+ return pdt
323
+
324
+ def hydrate_primitive(self, pdt):
325
+ """Attempt to hydrate a PrimitivePDT. Returns typed object or raw PDT."""
326
+ if not isinstance(pdt, PrimitivePDT):
327
+ return pdt
328
+ adapter = self._primitive_adapters_by_name.get(pdt.name)
329
+ if adapter is None:
330
+ return pdt
331
+ try:
332
+ return adapter['from_value'](pdt.value)
333
+ except Exception as e:
334
+ import logging
335
+ logging.getLogger(__name__).warning(f"Primitive PDT hydration failed for '{pdt.name}': {e}")
336
+ return pdt
337
+
338
+ def get_composite_adapter_by_class(self, cls):
339
+ """Return (type_name, serialize_fn) tuple for the given class, or None."""
340
+ return self._composite_adapters_by_class.get(cls)
341
+
342
+ def get_primitive_adapter_by_class(self, cls):
343
+ """Return adapter dict for the given class, or None."""
344
+ return self._primitive_adapters_by_class.get(cls)
345
+
346
+
347
+ # Module-level registry of @provider_defined decorated classes keyed by PDT name.
348
+ _pdt_decorated_types = {}
349
+
350
+
351
+ def provider_defined(name=None, included_fields=None, excluded_fields=None):
352
+ """Decorator that marks a class as a Provider Defined Type."""
353
+ def decorator(cls):
354
+ cls._pdt_name = name or cls.__name__
355
+ cls._pdt_included_fields = included_fields
356
+ cls._pdt_excluded_fields = excluded_fields
357
+ _pdt_decorated_types[cls._pdt_name] = cls
358
+ return cls
359
+ return decorator
360
+
361
+
362
+ class Tree(object):
363
+ """
364
+ A tree data structure with a tree-shaped public API.
365
+
366
+ Children are backed by an ordered list of ``(key, subtree)`` entries,
367
+ preserving insertion order and using value-equality (``==``) for key lookup
368
+ so keys need not be hashable. ``None`` keys are supported.
369
+ """
370
+
371
+ def __init__(self, entries=None):
372
+ # internal ordered list of [key, subtree] pairs
373
+ self._entries = []
374
+ if entries is not None:
375
+ for key, child in entries:
376
+ if not isinstance(child, Tree):
377
+ raise TypeError("Tree entries must map a key to a Tree, got: " + repr(child))
378
+ self._entries.append([key, child])
379
+
380
+ # ------------------------------------------------------------------
381
+ # internal helpers
382
+ # ------------------------------------------------------------------
383
+
384
+ def _find_entry(self, key):
385
+ for entry in self._entries:
386
+ if entry[0] == key:
387
+ return entry
388
+ return None
389
+
390
+ # ------------------------------------------------------------------
391
+ # navigation
392
+ # ------------------------------------------------------------------
393
+
394
+ def root_nodes(self):
395
+ """Returns the list of keys at the root of this tree, in insertion order."""
396
+ return [entry[0] for entry in self._entries]
397
+
398
+ def child_at(self, key):
399
+ """
400
+ Returns the child subtree for the given key.
401
+
402
+ :raises KeyError: if no immediate child exists for the given key
403
+ """
404
+ entry = self._find_entry(key)
405
+ if entry is None:
406
+ raise KeyError("Tree has no child for key: " + str(key))
407
+ return entry[1]
408
+
409
+ def has_child(self, key):
410
+ """Returns True if the given key is an immediate child of this tree."""
411
+ return self._find_entry(key) is not None
412
+
413
+ def contains(self, value):
414
+ """Returns True if the given value appears as a key anywhere in this tree (recursive)."""
415
+ for key, child in self._entries:
416
+ if key == value or child.contains(value):
417
+ return True
418
+ return False
419
+
420
+ def find_subtree(self, key):
421
+ """
422
+ Recursively searches the tree for the first subtree rooted at ``key`` and
423
+ returns it, or ``None`` if not found. Direct children are visited before
424
+ recursing.
425
+ """
426
+ for k, child in self._entries:
427
+ if k == key:
428
+ return child
429
+ for _, child in self._entries:
430
+ found = child.find_subtree(key)
431
+ if found is not None:
432
+ return found
433
+ return None
434
+
435
+ def get_or_create_child(self, key):
436
+ """Returns the existing child for ``key``, or inserts and returns a new empty Tree if absent."""
437
+ entry = self._find_entry(key)
438
+ if entry is None:
439
+ child = Tree()
440
+ self._entries.append([key, child])
441
+ return child
442
+ return entry[1]
443
+
444
+ # ------------------------------------------------------------------
445
+ # structural
446
+ # ------------------------------------------------------------------
447
+
448
+ def is_leaf(self):
449
+ """Returns True if this tree has no children. An empty tree is considered a leaf."""
450
+ return len(self._entries) == 0
451
+
452
+ def node_count(self):
453
+ """Returns the total number of nodes (keys) in the tree, counted recursively."""
454
+ count = len(self._entries)
455
+ for _, child in self._entries:
456
+ count += child.node_count()
457
+ return count
458
+
459
+ def get_nodes_at_depth(self, depth):
460
+ """
461
+ Returns the keys at the given depth. Depth 0 returns the root keys.
462
+
463
+ Negative depths and depths beyond the tree's height return an empty list.
464
+ """
465
+ nodes = []
466
+ for tree in self.get_trees_at_depth(depth):
467
+ nodes.extend(tree.root_nodes())
468
+ return nodes
469
+
470
+ def get_trees_at_depth(self, depth):
471
+ """
472
+ Returns the trees at the given depth. Depth 0 returns a singleton list
473
+ containing this tree. Negative depths and depths beyond the tree's
474
+ height return an empty list.
475
+ """
476
+ if depth < 0:
477
+ return []
478
+ current = [self]
479
+ for _ in range(depth):
480
+ nxt = []
481
+ for tree in current:
482
+ nxt.extend(entry[1] for entry in tree._entries)
483
+ if not nxt:
484
+ return []
485
+ current = nxt
486
+ return current
487
+
488
+ def get_leaf_nodes(self):
489
+ """Returns all keys whose subtrees are leaves (recursive)."""
490
+ leaves = []
491
+ self._collect_leaf_keys(leaves)
492
+ return leaves
493
+
494
+ def _collect_leaf_keys(self, out):
495
+ for key, child in self._entries:
496
+ if child.is_leaf():
497
+ out.append(key)
498
+ else:
499
+ child._collect_leaf_keys(out)
500
+
501
+ def get_leaf_trees(self):
502
+ """Returns single-key trees representing each leaf key in this tree (recursive)."""
503
+ leaves = []
504
+ self._collect_leaf_trees(leaves)
505
+ return leaves
506
+
507
+ def _collect_leaf_trees(self, out):
508
+ for key, child in self._entries:
509
+ if child.is_leaf():
510
+ out.append(Tree([(key, child)]))
511
+ else:
512
+ child._collect_leaf_trees(out)
513
+
514
+ # ------------------------------------------------------------------
515
+ # composition
516
+ # ------------------------------------------------------------------
517
+
518
+ def add_tree(self, other):
519
+ """
520
+ Recursively merges ``other`` into this tree. For overlapping keys (by
521
+ value-equality) child subtrees are merged in turn. For keys present only
522
+ in ``other`` the corresponding subtree reference is adopted directly.
523
+ """
524
+ for key, child in other._entries:
525
+ entry = self._find_entry(key)
526
+ if entry is None:
527
+ self._entries.append([key, child])
528
+ else:
529
+ entry[1].add_tree(child)
530
+
531
+ def split_parents(self):
532
+ """
533
+ Splits this tree into one tree per root key. If the tree has a single
534
+ root, returns a singleton list containing this tree.
535
+ """
536
+ if len(self._entries) == 1:
537
+ return [self]
538
+ return [Tree([(key, child)]) for key, child in self._entries]
539
+
540
+ # ------------------------------------------------------------------
541
+ # output
542
+ # ------------------------------------------------------------------
543
+
544
+ def pretty_print(self):
545
+ """
546
+ Produces a formatted string representation of the tree structure using a
547
+ ``|--`` ASCII style, matching Java's ``Tree.prettyPrint``.
548
+
549
+ Each level is indented by 3 spaces relative to its parent. The returned
550
+ string has no trailing newline.
551
+ """
552
+ lines = []
553
+ self._pretty_print(lines, "")
554
+ return "\n".join(lines)
555
+
556
+ def _pretty_print(self, lines, prefix):
557
+ for key, child in self._entries:
558
+ lines.append(prefix + "|--" + str(key))
559
+ child._pretty_print(lines, prefix + " ")
560
+
561
+ # ------------------------------------------------------------------
562
+ # identity
563
+ # ------------------------------------------------------------------
564
+
565
+ def __eq__(self, other):
566
+ if not isinstance(other, Tree):
567
+ return NotImplemented
568
+ if len(self._entries) != len(other._entries):
569
+ return False
570
+ for key, child in self._entries:
571
+ matched = False
572
+ for okey, ochild in other._entries:
573
+ if okey == key:
574
+ if ochild != child:
575
+ return False
576
+ matched = True
577
+ break
578
+ if not matched:
579
+ return False
580
+ return True
581
+
582
+ def __hash__(self):
583
+ # structurally-equal trees share the same number of root entries, so this
584
+ # is consistent with __eq__ while remaining valid for unhashable keys.
585
+ return hash(len(self._entries))
586
+
587
+ def __repr__(self):
588
+ return "{" + ", ".join(str(key) + "=" + repr(child) for key, child in self._entries) + "}"