Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions graphistry/compute/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .ComputeMixin import ComputeMixin
from .ast import (
n, e, e_forward, e_reverse, e_undirected,
let, remote, ref
let, remote, ref, call
)
from .chain import Chain
from .predicates.is_in import (
Expand Down Expand Up @@ -61,7 +61,7 @@
'ComputeMixin', 'Chain',
# AST nodes
'n', 'e', 'e_forward', 'e_reverse', 'e_undirected',
'let', 'remote', 'ref',
'let', 'remote', 'ref', 'call',
# Predicates
'is_in', 'IsIn',
'duplicated', 'Duplicated',
Expand Down
123 changes: 118 additions & 5 deletions graphistry/compute/ast.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import abstractmethod
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast
from typing import Any, TYPE_CHECKING, Dict, List, Optional, Union, cast
from typing_extensions import Literal

if TYPE_CHECKING:
Expand Down Expand Up @@ -738,20 +738,130 @@ def reverse(self) -> 'ASTRef':
return ASTRef(self.ref, [op.reverse() for op in reversed(self.chain)])


class ASTCall(ASTObject):
"""Call a method on the current graph with validated parameters.

Allows safe execution of Plottable methods through GFQL with parameter
validation and schema checking.

Attributes:
function: Name of the method to call (must be in safelist)
params: Dictionary of parameters to pass to the method
"""
def __init__(self, function: str, params: Optional[Dict[str, Any]] = None):
"""Initialize a Call operation.

Args:
function: Name of the Plottable method to call
params: Optional dictionary of parameters for the method
"""
super().__init__()
self.function = function
self.params = params or {}

def _validate_fields(self) -> None:
"""Validate Call fields."""
from graphistry.compute.exceptions import ErrorCode, GFQLTypeError

if not isinstance(self.function, str):
raise GFQLTypeError(
ErrorCode.E201,
"function must be a string",
field="function",
value=type(self.function).__name__
)

if len(self.function) == 0:
raise GFQLTypeError(
ErrorCode.E106,
"function name cannot be empty",
field="function",
value=self.function
)

if not isinstance(self.params, dict):
raise GFQLTypeError(
ErrorCode.E201,
"params must be a dictionary",
field="params",
value=type(self.params).__name__
)

def to_json(self, validate=True) -> dict:
"""Convert Call to JSON representation.

Args:
validate: If True, validate before serialization

Returns:
Dictionary with type, function, and params fields
"""
if validate:
self.validate()
return {
'type': 'Call',
'function': self.function,
'params': self.params
}

@classmethod
def from_json(cls, d: dict, validate: bool = True) -> 'ASTCall':
assert 'function' in d, "Call missing function"
out = cls(
function=d['function'],
params=d.get('params', {})
)
if validate:
out.validate()
return out

def __call__(self, g: Plottable, prev_node_wavefront: Optional[DataFrameT],
target_wave_front: Optional[DataFrameT], engine: Engine) -> Plottable:
"""Execute the method call on the graph.

Args:
g: Graph to operate on
prev_node_wavefront: Previous node wavefront (unused)
target_wave_front: Target wavefront (unused)
engine: Execution engine (pandas/cudf)

Returns:
New Plottable with method results

Raises:
GFQLTypeError: If method not in safelist or parameters invalid
"""
# For chain_dag, we don't use wavefronts, just execute the call
from graphistry.compute.call_executor import execute_call
return execute_call(g, self.function, self.params, engine)

def reverse(self) -> 'ASTCall':
"""Reverse is not supported for Call operations.

Most Plottable methods are not reversible as they perform
transformations that cannot be undone.

Raises:
NotImplementedError: Always raised as calls cannot be reversed
"""
# Most method calls cannot be reversed
raise NotImplementedError(f"Method '{self.function}' cannot be reversed")


###

def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge, ASTLet, ASTRemoteGraph, ASTRef]:
def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge, ASTLet, ASTRemoteGraph, ASTRef, ASTCall]:
from graphistry.compute.exceptions import ErrorCode, GFQLSyntaxError

if not isinstance(o, dict):
raise GFQLSyntaxError(ErrorCode.E101, "AST JSON must be a dictionary", value=type(o).__name__)

if 'type' not in o:
raise GFQLSyntaxError(
ErrorCode.E105, "AST JSON missing required 'type' field", suggestion="Add 'type' field: 'Node', 'Edge', 'QueryDAG', 'RemoteGraph', or 'Ref'"
ErrorCode.E105, "AST JSON missing required 'type' field", suggestion="Add 'type' field: 'Node', 'Edge', 'QueryDAG', 'RemoteGraph', 'Ref', or 'Call'"
)

out: Union[ASTNode, ASTEdge, ASTLet, ASTRemoteGraph, ASTRef]
out: Union[ASTNode, ASTEdge, ASTLet, ASTRemoteGraph, ASTRef, ASTCall]
if o['type'] == 'Node':
out = ASTNode.from_json(o, validate=validate)
elif o['type'] == 'Edge':
Expand Down Expand Up @@ -783,13 +893,15 @@ def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge, ASTL
out = ASTRemoteGraph.from_json(o, validate=validate)
elif o['type'] == 'Ref':
out = ASTRef.from_json(o, validate=validate)
elif o['type'] == 'Call':
out = ASTCall.from_json(o, validate=validate)
else:
raise GFQLSyntaxError(
ErrorCode.E101,
f"Unknown AST type: {o['type']}",
field="type",
value=o["type"],
suggestion="Use 'Node', 'Edge', 'Let', 'RemoteGraph', or 'Ref'",
suggestion="Use 'Node', 'Edge', 'Let', 'RemoteGraph', 'Ref', or 'Call'",
)
return out

Expand All @@ -800,3 +912,4 @@ def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge, ASTL
let = ASTLet # noqa: E305
remote = ASTRemoteGraph # noqa: E305
ref = ASTRef # noqa: E305
call = ASTCall # noqa: E305
83 changes: 83 additions & 0 deletions graphistry/compute/call_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Execute validated method calls on Plottable objects.

This module provides the execution layer for GFQL Call operations after
parameter validation has been performed by the safelist module.
"""

from typing import Dict, Any

from graphistry.Plottable import Plottable
from graphistry.Engine import Engine
from graphistry.compute.call_safelist import validate_call_params
from graphistry.compute.exceptions import ErrorCode, GFQLTypeError


def execute_call(g: Plottable, function: str, params: Dict[str, Any], engine: Engine) -> Plottable:
"""Execute a validated method call on a Plottable.

Args:
g: The graph to call the method on
function: Name of the method to call
params: Parameters for the method (will be validated)
engine: Execution engine

Returns:
Result of the method call (usually a new Plottable)

Raises:
GFQLTypeError: If validation fails or method doesn't exist
AttributeError: If method doesn't exist on Plottable
"""
# Validate parameters against safelist
validated_params = validate_call_params(function, params)

# Check if method exists on Plottable
if not hasattr(g, function):
raise AttributeError(
f"Plottable has no method '{function}'. "
f"This should not happen if safelist is properly configured."
)

# Get the method
method = getattr(g, function)

# Special handling for methods that need the engine parameter
if function in ['materialize_nodes', 'hop']:
# These methods accept an engine parameter
if 'engine' not in validated_params:
# Add current engine if not specified
validated_params['engine'] = engine

try:
# Execute the method with validated parameters
result = method(**validated_params)

# Ensure result is a Plottable (most methods return self or new Plottable)
if not isinstance(result, Plottable):
raise GFQLTypeError(
ErrorCode.E201,
f"Method '{function}' returned non-Plottable result",
field="function",
value=f"{type(result).__name__}",
suggestion="Only methods that return Plottable objects are allowed"
)

return result

except TypeError as e:
# Handle parameter mismatch errors
raise GFQLTypeError(
ErrorCode.E201,
f"Parameter error calling '{function}': {str(e)}",
field="params",
value=validated_params,
suggestion="Check parameter names and types"
) from e
except Exception as e:
# Re-raise other exceptions with context
raise GFQLTypeError(
ErrorCode.E303,
f"Error executing '{function}': {str(e)}",
field="function",
value=function
) from e
Loading
Loading