blob: 0b8ddb89e31efed71c26221ee2137d5021dfe698 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
__author__ = 'Marko A. Rodriguez (http://markorodriguez.com)'
class Graph(object):
def __init__(self):
self.vertices = {}
self.edges = {}
def __repr__(self):
return "graph[vertices:" + str(len(self.vertices)) + " edges:" + str(len(self.edges)) + "]"
class Element(object):
def __init__(self, id, label, properties=None):
self.id = id
self.label = label
self.properties = [] if properties is None else properties
def __getitem__(self, key):
for p in self.properties:
if p.key == key:
return p.value
raise KeyError(key)
def __contains__(self, key):
for p in self.properties:
if p.key == key:
return True
return False
def keys(self):
return set(p.key for p in self.properties)
def values(self, *property_keys):
if len(property_keys) == 0:
return [p.value for p in self.properties]
else:
return [p.value for p in self.properties if p.key in property_keys]
def __eq__(self, other):
return isinstance(other, self.__class__) and self.id == other.id
def __hash__(self):
return hash(self.id)
def property_map(self):
"""Groups this element's properties by key.
Returns a dict of property key -> list of property objects (an empty
dict when this element has no properties). ``self.properties`` is a
flat list where each item exposes ``.key``.
"""
result = {}
for p in self.properties:
result.setdefault(p.key, []).append(p)
return result
class Vertex(Element):
def __init__(self, id, label="vertex", properties=None, labels=None):
if labels is not None:
self._labels = set(labels)
Element.__init__(self, id, next(iter(labels)) if labels else "", properties)
else:
self._labels = {label} if label else {"vertex"}
Element.__init__(self, id, label or "vertex", properties)
@property
def labels(self):
return frozenset(self._labels)
def __repr__(self):
return "v[" + str(self.id) + "]"
class Edge(Element):
def __init__(self, id, outV, label, inV, properties=None, labels=None):
if labels is not None:
self._labels = set(labels)
Element.__init__(self, id, next(iter(labels)) if labels else "", properties)
else:
self._labels = {label} if label else {"edge"}
Element.__init__(self, id, label or "edge", properties)
self.outV = outV
self.inV = inV
@property
def labels(self):
return frozenset(self._labels)
def __repr__(self):
return "e[" + str(self.id) + "][" + str(self.outV.id) + "-" + self.label + "->" + str(self.inV.id) + "]"
class VertexProperty(Element):
def __init__(self, id, label, value, vertex, properties=None):
Element.__init__(self, id, label, properties)
self.value = value
self.key = self.label
self.vertex = vertex
def __repr__(self):
return "vp[" + str(self.label) + "->" + str(self.value)[0:20] + "]"
class Property(object):
def __init__(self, key, value, element):
self.key = key
self.value = value
self.element = element
def __repr__(self):
return "p[" + str(self.key) + "->" + str(self.value)[0:20] + "]"
def __eq__(self, other):
return isinstance(other, self.__class__) and \
self.key == other.key and \
self.value == other.value and \
self.element == other.element
def __hash__(self):
return hash(self.key) + hash(self.value)
class Path(object):
def __init__(self, labels, objects):
self.labels = labels
self.objects = objects
def __repr__(self):
return "path[" + ", ".join(map(str, self.objects)) + "]"
def __eq__(self, other):
return isinstance(other, self.__class__) and self.objects == other.objects and self.labels == other.labels
def __hash__(self):
return hash(str(self.objects)) + hash(str(self.labels))
def __getitem__(self, key):
if isinstance(key, str):
objects = []
for i, labels in enumerate(self.labels):
if key in labels:
objects.append(self.objects[i])
if 0 == len(objects):
raise KeyError("The step with label " + key + " does not exist")
return objects if len(objects) > 1 else objects[0]
elif isinstance(key, int):
return self.objects[key]
else:
raise TypeError("The path access key must be either a string label or integer index")
def __len__(self):
return len(self.objects)
class CompositePDT(object):
def __init__(self, name, fields):
if not name:
raise ValueError("name cannot be null or empty")
self._name = name
self._fields = dict(fields) if fields else {}
if any(not isinstance(k, str) for k in self._fields):
raise TypeError("CompositePDT field keys must be strings")
@property
def name(self):
return self._name
@property
def fields(self):
return self._fields
def __eq__(self, other):
return isinstance(other, CompositePDT) and self._name == other._name and self._fields == other._fields
def __hash__(self):
try:
return hash((self._name, frozenset(self._fields.items())))
except TypeError:
return hash(self._name)
def __repr__(self):
return f"pdt[{self._name}]{self._fields}"
class PrimitivePDT(object):
"""An immutable primitive provider-defined type consisting of a name and an opaque string value."""
def __init__(self, name, value):
if not name:
raise ValueError("name cannot be null or empty")
if value is None:
raise ValueError("value cannot be null")
self._name = name
self._value = value
@property
def name(self):
return self._name
@property
def value(self):
return self._value
def __eq__(self, other):
return isinstance(other, PrimitivePDT) and self._name == other._name and self._value == other._value
def __hash__(self):
return hash((self._name, self._value))
def __repr__(self):
return f"pdt[{self._name}]({self._value})"
class PDTRegistry(object):
def __init__(self):
self._composite_adapters_by_name = {}
self._composite_adapters_by_class = {}
self._primitive_adapters_by_name = {}
self._primitive_adapters_by_class = {}
def register(self, type_name, deserialize_fn, serialize_fn=None, target_class=None):
self._composite_adapters_by_name[type_name] = {
'deserialize': deserialize_fn,
'serialize': serialize_fn,
'target_class': target_class
}
if target_class is not None:
self._composite_adapters_by_class[target_class] = {
'type_name': type_name,
'serialize': serialize_fn,
}
def register_primitive(self, type_name, from_value, to_value=None, target_class=None):
"""Register a primitive PDT adapter.
Args:
type_name: The PDT type name string.
from_value: Callable(str) -> object for deserialization.
to_value: Callable(object) -> str for serialization (optional).
target_class: The Python class this adapter produces (optional).
"""
self._primitive_adapters_by_name[type_name] = {
'from_value': from_value,
'to_value': to_value,
'target_class': target_class
}
if target_class is not None:
self._primitive_adapters_by_class[target_class] = {
'type_name': type_name,
'to_value': to_value,
}
@classmethod
def create(cls):
"""Create a registry populated by entry_points discovery.
Providers register adapters via pyproject.toml:
[project.entry-points."tinkerpop.pdt"]
my_types = "my_package:register_pdt_types"
Each entry point should be a callable that accepts a registry and registers adapters.
"""
import sys
registry = cls()
if sys.version_info >= (3, 10):
from importlib.metadata import entry_points
eps = entry_points(group='tinkerpop.pdt')
else:
from importlib.metadata import entry_points
all_eps = entry_points()
eps = all_eps.get('tinkerpop.pdt', [])
for ep in eps:
try:
factory = ep.load()
factory(registry)
except Exception as e:
import logging
logging.getLogger(__name__).warning(
f"Failed to load PDT adapter from entry point '{ep.name}': {e}")
return registry
def hydrate(self, pdt):
"""Attempt to hydrate a CompositePDT. Returns typed object or raw PDT."""
if not isinstance(pdt, CompositePDT):
return pdt
# Always recurse into fields to hydrate nested registered PDTs.
changed = False
hydrated_fields = {}
for k, v in pdt.fields.items():
if isinstance(v, CompositePDT):
h = self.hydrate(v)
if h is not v:
changed = True
hydrated_fields[k] = h
elif isinstance(v, PrimitivePDT):
h = self.hydrate_primitive(v)
if h is not v:
changed = True
hydrated_fields[k] = h
else:
hydrated_fields[k] = v
adapter = self._composite_adapters_by_name.get(pdt.name)
if adapter is None:
return CompositePDT(pdt.name, hydrated_fields) if changed else pdt
try:
return adapter['deserialize'](hydrated_fields)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"PDT hydration failed for '{pdt.name}': {e}")
return pdt
def hydrate_primitive(self, pdt):
"""Attempt to hydrate a PrimitivePDT. Returns typed object or raw PDT."""
if not isinstance(pdt, PrimitivePDT):
return pdt
adapter = self._primitive_adapters_by_name.get(pdt.name)
if adapter is None:
return pdt
try:
return adapter['from_value'](pdt.value)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Primitive PDT hydration failed for '{pdt.name}': {e}")
return pdt
def get_composite_adapter_by_class(self, cls):
"""Return (type_name, serialize_fn) tuple for the given class, or None."""
return self._composite_adapters_by_class.get(cls)
def get_primitive_adapter_by_class(self, cls):
"""Return adapter dict for the given class, or None."""
return self._primitive_adapters_by_class.get(cls)
# Module-level registry of @provider_defined decorated classes keyed by PDT name.
_pdt_decorated_types = {}
def provider_defined(name=None, included_fields=None, excluded_fields=None):
"""Decorator that marks a class as a Provider Defined Type."""
def decorator(cls):
cls._pdt_name = name or cls.__name__
cls._pdt_included_fields = included_fields
cls._pdt_excluded_fields = excluded_fields
_pdt_decorated_types[cls._pdt_name] = cls
return cls
return decorator
class Tree(object):
"""
A tree data structure with a tree-shaped public API.
Children are backed by an ordered list of ``(key, subtree)`` entries,
preserving insertion order and using value-equality (``==``) for key lookup
so keys need not be hashable. ``None`` keys are supported.
"""
def __init__(self, entries=None):
# internal ordered list of [key, subtree] pairs
self._entries = []
if entries is not None:
for key, child in entries:
if not isinstance(child, Tree):
raise TypeError("Tree entries must map a key to a Tree, got: " + repr(child))
self._entries.append([key, child])
# ------------------------------------------------------------------
# internal helpers
# ------------------------------------------------------------------
def _find_entry(self, key):
for entry in self._entries:
if entry[0] == key:
return entry
return None
# ------------------------------------------------------------------
# navigation
# ------------------------------------------------------------------
def root_nodes(self):
"""Returns the list of keys at the root of this tree, in insertion order."""
return [entry[0] for entry in self._entries]
def child_at(self, key):
"""
Returns the child subtree for the given key.
:raises KeyError: if no immediate child exists for the given key
"""
entry = self._find_entry(key)
if entry is None:
raise KeyError("Tree has no child for key: " + str(key))
return entry[1]
def has_child(self, key):
"""Returns True if the given key is an immediate child of this tree."""
return self._find_entry(key) is not None
def contains(self, value):
"""Returns True if the given value appears as a key anywhere in this tree (recursive)."""
for key, child in self._entries:
if key == value or child.contains(value):
return True
return False
def find_subtree(self, key):
"""
Recursively searches the tree for the first subtree rooted at ``key`` and
returns it, or ``None`` if not found. Direct children are visited before
recursing.
"""
for k, child in self._entries:
if k == key:
return child
for _, child in self._entries:
found = child.find_subtree(key)
if found is not None:
return found
return None
def get_or_create_child(self, key):
"""Returns the existing child for ``key``, or inserts and returns a new empty Tree if absent."""
entry = self._find_entry(key)
if entry is None:
child = Tree()
self._entries.append([key, child])
return child
return entry[1]
# ------------------------------------------------------------------
# structural
# ------------------------------------------------------------------
def is_leaf(self):
"""Returns True if this tree has no children. An empty tree is considered a leaf."""
return len(self._entries) == 0
def node_count(self):
"""Returns the total number of nodes (keys) in the tree, counted recursively."""
count = len(self._entries)
for _, child in self._entries:
count += child.node_count()
return count
def get_nodes_at_depth(self, depth):
"""
Returns the keys at the given depth. Depth 0 returns the root keys.
Negative depths and depths beyond the tree's height return an empty list.
"""
nodes = []
for tree in self.get_trees_at_depth(depth):
nodes.extend(tree.root_nodes())
return nodes
def get_trees_at_depth(self, depth):
"""
Returns the trees at the given depth. Depth 0 returns a singleton list
containing this tree. Negative depths and depths beyond the tree's
height return an empty list.
"""
if depth < 0:
return []
current = [self]
for _ in range(depth):
nxt = []
for tree in current:
nxt.extend(entry[1] for entry in tree._entries)
if not nxt:
return []
current = nxt
return current
def get_leaf_nodes(self):
"""Returns all keys whose subtrees are leaves (recursive)."""
leaves = []
self._collect_leaf_keys(leaves)
return leaves
def _collect_leaf_keys(self, out):
for key, child in self._entries:
if child.is_leaf():
out.append(key)
else:
child._collect_leaf_keys(out)
def get_leaf_trees(self):
"""Returns single-key trees representing each leaf key in this tree (recursive)."""
leaves = []
self._collect_leaf_trees(leaves)
return leaves
def _collect_leaf_trees(self, out):
for key, child in self._entries:
if child.is_leaf():
out.append(Tree([(key, child)]))
else:
child._collect_leaf_trees(out)
# ------------------------------------------------------------------
# composition
# ------------------------------------------------------------------
def add_tree(self, other):
"""
Recursively merges ``other`` into this tree. For overlapping keys (by
value-equality) child subtrees are merged in turn. For keys present only
in ``other`` the corresponding subtree reference is adopted directly.
"""
for key, child in other._entries:
entry = self._find_entry(key)
if entry is None:
self._entries.append([key, child])
else:
entry[1].add_tree(child)
def split_parents(self):
"""
Splits this tree into one tree per root key. If the tree has a single
root, returns a singleton list containing this tree.
"""
if len(self._entries) == 1:
return [self]
return [Tree([(key, child)]) for key, child in self._entries]
# ------------------------------------------------------------------
# output
# ------------------------------------------------------------------
def pretty_print(self):
"""
Produces a formatted string representation of the tree structure using a
``|--`` ASCII style, matching Java's ``Tree.prettyPrint``.
Each level is indented by 3 spaces relative to its parent. The returned
string has no trailing newline.
"""
lines = []
self._pretty_print(lines, "")
return "\n".join(lines)
def _pretty_print(self, lines, prefix):
for key, child in self._entries:
lines.append(prefix + "|--" + str(key))
child._pretty_print(lines, prefix + " ")
# ------------------------------------------------------------------
# identity
# ------------------------------------------------------------------
def __eq__(self, other):
if not isinstance(other, Tree):
return NotImplemented
if len(self._entries) != len(other._entries):
return False
for key, child in self._entries:
matched = False
for okey, ochild in other._entries:
if okey == key:
if ochild != child:
return False
matched = True
break
if not matched:
return False
return True
def __hash__(self):
# structurally-equal trees share the same number of root entries, so this
# is consistent with __eq__ while remaining valid for unhashable keys.
return hash(len(self._entries))
def __repr__(self):
return "{" + ", ".join(str(key) + "=" + repr(child) for key, child in self._entries) + "}"