Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f8c01d0
feat(gfql): add core AST nodes for GFQL Programs
lmeyerov Jul 19, 2025
465981c
feat(gfql): implement DAG execution with dependency resolution
lmeyerov Jul 19, 2025
9bf05bb
feat(gfql): add unified API with deprecation migration
lmeyerov Jul 19, 2025
7f6cc9e
fix(gfql): improve JSON serialization error messages
lmeyerov Jul 19, 2025
e007704
feat(gfql): add schema validation and remote graph support
lmeyerov Jul 19, 2025
3f0765a
feat(gfql): add user-friendly aliases for AST classes
lmeyerov Jul 20, 2025
73cf7ed
fix: clean up lint, type, and test issues after rebase
lmeyerov Jul 23, 2025
184e6e4
fix(lint): fix flake8 and mypy type errors in PR #706
lmeyerov Jul 24, 2025
9585397
fix(ci): trigger CI re-run to verify lint fixes
lmeyerov Jul 24, 2025
e8ef1bb
fix(lint): add whitespace around arithmetic operator in f-string
lmeyerov Jul 24, 2025
b52faa5
fix(test): correct import path for validate_schema module
lmeyerov Jul 24, 2025
554e558
refactor(ast): rename ASTQueryDAG to ASTLet for clarity
lmeyerov Jul 24, 2025
d64cf6a
refactor(compute): update imports and references to use ASTLet
lmeyerov Jul 24, 2025
4296078
test: rename test_querydag.py to test_let.py
lmeyerov Jul 24, 2025
dbf4df6
test: update all tests to use ASTLet instead of ASTQueryDAG
lmeyerov Jul 24, 2025
967f4a7
test: fix numpy.bool_ comparison in chain_dag tests
lmeyerov Jul 24, 2025
e27f16e
chore: ignore test_env directory
lmeyerov Jul 24, 2025
34f1abe
fix(gfql): update ASTLet.__call__ error message to reflect implementa…
lmeyerov Jul 24, 2025
36a7010
fix(gfql): implement ASTLet.__call__ to proxy to chain_dag_impl
lmeyerov Jul 24, 2025
4901f9e
fix(lint): fix critical import and lint issues in PR #706
lmeyerov Jul 24, 2025
e15a6d6
fix(types): fix mypy type errors in ast.py, chain_dag.py, and validat…
lmeyerov Jul 24, 2025
15de494
fix(lint): add newline at end of __init__.py
lmeyerov Jul 24, 2025
a9b8ff0
fix(tests): correct import paths for is_in predicate in test files
lmeyerov Jul 25, 2025
ccdc867
refactor(gfql): rename ASTChainRef to ASTRef for cleaner terminology
lmeyerov Jul 26, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ docs/source/demos
# AI assistant working directories
AI_PROGRESS/
PLAN.md
test_env/
29 changes: 26 additions & 3 deletions graphistry/compute/ComputeMixin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import numpy as np, pandas as pd
import numpy as np
import pandas as pd
from typing import Any, List, Union
from inspect import getmodule

from graphistry.Engine import Engine, EngineAbstract
from graphistry.Plottable import Plottable
from graphistry.util import setup_logger
from .chain import chain as chain_base
from .chain_remote import chain_remote as chain_remote_base, chain_remote_shape as chain_remote_shape_base
from .gfql import gfql as gfql_base
from .chain_remote import (
chain_remote as chain_remote_base,
chain_remote_shape as chain_remote_shape_base
)
from .python_remote import (
python_remote_g as python_remote_g_base,
python_remote_table as python_remote_table_base,
Expand Down Expand Up @@ -460,8 +465,26 @@ def filter_edges_by_dict(self, *args, **kwargs):
filter_edges_by_dict.__doc__ = filter_edges_by_dict_base.__doc__

def chain(self, *args, **kwargs):
"""
.. deprecated:: 2.XX.X
Use :meth:`gfql` instead for a unified API that supports both chains and DAGs.
"""
import warnings
warnings.warn(
"chain() is deprecated. Use gfql() instead for a unified API.",
DeprecationWarning,
stacklevel=2
)
return chain_base(self, *args, **kwargs)
chain.__doc__ = chain_base.__doc__
# Preserve original docstring after deprecation notice
chain.__doc__ = (chain.__doc__ or "") + "\n\n" + (chain_base.__doc__ or "")

# chain_dag removed from public API - use gfql() instead
# (chain_dag_base still available internally for gfql dispatch)

def gfql(self, *args, **kwargs):
return gfql_base(self, *args, **kwargs)
gfql.__doc__ = gfql_base.__doc__

def chain_remote(self, *args, **kwargs) -> Plottable:
return chain_remote_base(self, *args, **kwargs)
Expand Down
39 changes: 38 additions & 1 deletion graphistry/compute/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .ComputeMixin import ComputeMixin
from .ast import (
n, e, e_forward, e_reverse, e_undirected
n, e, e_forward, e_reverse, e_undirected,
let, remote, ref
)
from .chain import Chain
from .predicates.is_in import (
Expand Down Expand Up @@ -54,3 +55,39 @@
notnull, NotNull,
)
from .typing import DataFrameT

__all__ = [
# Core classes
'ComputeMixin', 'Chain',
# AST nodes
'n', 'e', 'e_forward', 'e_reverse', 'e_undirected',
'let', 'remote', 'ref',
# Predicates
'is_in', 'IsIn',
'duplicated', 'Duplicated',
'is_month_start', 'IsMonthStart',
'is_month_end', 'IsMonthEnd',
'is_quarter_start', 'IsQuarterStart',
'is_quarter_end', 'IsQuarterEnd',
'is_year_start', 'IsYearStart',
'is_year_end', 'IsYearEnd',
'is_leap_year', 'IsLeapYear',
# Temporal
'TemporalValue', 'DateTimeValue', 'DateValue', 'TimeValue',
'temporal_value_from_json',
# Comparison predicates
'gt', 'GT', 'lt', 'LT', 'ge', 'GE', 'le', 'LE',
'eq', 'EQ', 'ne', 'NE', 'between', 'Between',
'isna', 'IsNA', 'notna', 'NotNA',
# String predicates
'contains', 'Contains', 'startswith', 'Startswith',
'endswith', 'Endswith', 'match', 'Match',
'isnumeric', 'IsNumeric', 'isalpha', 'IsAlpha',
'isdigit', 'IsDigit', 'islower', 'IsLower',
'isupper', 'IsUpper', 'isspace', 'IsSpace',
'isalnum', 'IsAlnum', 'isdecimal', 'IsDecimal',
'istitle', 'IsTitle', 'isnull', 'IsNull',
'notnull', 'NotNull',
# Types
'DataFrameT'
]
211 changes: 161 additions & 50 deletions graphistry/compute/ast.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from abc import abstractmethod
import logging
from typing import Any, TYPE_CHECKING, Dict, Optional, Union, cast
from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast
from typing_extensions import Literal
import pandas as pd

if TYPE_CHECKING:
from graphistry.compute.exceptions import GFQLValidationError
from graphistry.Engine import Engine

from graphistry.Plottable import Plottable
Expand All @@ -12,50 +14,6 @@
from .predicates.ASTPredicate import ASTPredicate
from .predicates.from_json import from_json as predicates_from_json

from .predicates.is_in import (
is_in, IsIn
)
from .predicates.categorical import (
duplicated, Duplicated,
)
from .predicates.temporal import (
is_month_start, IsMonthStart,
is_month_end, IsMonthEnd,
is_quarter_start, IsQuarterStart,
is_quarter_end, IsQuarterEnd,
is_year_start, IsYearStart,
is_year_end, IsYearEnd,
is_leap_year, IsLeapYear
)
from .predicates.numeric import (
gt, GT,
lt, LT,
ge, GE,
le, LE,
eq, EQ,
ne, NE,
between, Between,
isna, IsNA,
notna, NotNA
)
from .predicates.str import (
contains, Contains,
startswith, Startswith,
endswith, Endswith,
match, Match,
isnumeric, IsNumeric,
isalpha, IsAlpha,
isdigit, IsDigit,
islower, IsLower,
isupper, IsUpper,
isspace, IsSpace,
isalnum, IsAlnum,
isdecimal, IsDecimal,
istitle, IsTitle,
isnull, IsNull,
notnull, NotNull
)
from .filter_by_dict import filter_by_dict
from .typing import DataFrameT


Expand Down Expand Up @@ -642,20 +600,158 @@ def from_json(cls, d: dict, validate: bool = True) -> 'ASTEdge':
e_undirected = ASTEdgeUndirected # noqa: E305
e = ASTEdgeUndirected # noqa: E305


##############################################################################


class ASTLet(ASTObject):
"""Let bindings for named graph operations"""
def __init__(self, bindings: Dict[str, ASTObject]):
super().__init__()
self.bindings = bindings

def validate(self, collect_all: bool = False) -> Optional[List['GFQLValidationError']]:
assert isinstance(self.bindings, dict), "bindings must be a dictionary"
for k, v in self.bindings.items():
assert isinstance(k, str), f"binding key must be string, got {type(k)}"
assert isinstance(v, ASTObject), f"binding value must be ASTObject, got {type(v)}"
v.validate()
# TODO: Check for cycles in DAG
return None

def to_json(self, validate=True) -> dict:
if validate:
self.validate()
return {
'type': 'Let',
'bindings': {k: v.to_json() for k, v in self.bindings.items()}
}

@classmethod
def from_json(cls, d: dict, validate: bool = True) -> 'ASTLet':
assert 'bindings' in d, "Let missing bindings"
bindings = {k: cast(ASTObject, from_json(v, validate=validate)) for k, v in d['bindings'].items()}
out = cls(bindings=bindings)
if validate:
out.validate()
return out

def __call__(self, g: Plottable, prev_node_wavefront: Optional[DataFrameT],
target_wave_front: Optional[DataFrameT], engine: Engine) -> Plottable:
# Let bindings don't use wavefronts - execute via chain_dag_impl
from graphistry.compute.chain_dag import chain_dag_impl
from graphistry.Engine import EngineAbstract
return chain_dag_impl(g, self, EngineAbstract(engine.value))

def reverse(self) -> 'ASTLet':
raise NotImplementedError("Let reversal not supported")


class ASTRemoteGraph(ASTObject):
"""Load a graph from Graphistry server"""
def __init__(self, dataset_id: str, token: Optional[str] = None):
super().__init__()
self.dataset_id = dataset_id
self.token = token

def validate(self, collect_all: bool = False) -> Optional[List['GFQLValidationError']]:
assert isinstance(self.dataset_id, str), "dataset_id must be a string"
assert len(self.dataset_id) > 0, "dataset_id cannot be empty"
assert self.token is None or isinstance(self.token, str), "token must be string or None"
return None

def to_json(self, validate=True) -> dict:
if validate:
self.validate()
result = {
'type': 'RemoteGraph',
'dataset_id': self.dataset_id
}
if self.token is not None:
result['token'] = self.token
return result

@classmethod
def from_json(cls, d: dict, validate: bool = True) -> 'ASTRemoteGraph':
assert 'dataset_id' in d, "RemoteGraph missing dataset_id"
out = cls(
dataset_id=d['dataset_id'],
token=d.get('token')
)
if validate:
out.validate()
return out

def __call__(self, g: Plottable, prev_node_wavefront: Optional[DataFrameT],
target_wave_front: Optional[DataFrameT], engine: Engine) -> Plottable:
# Implementation in PR 1.3
raise NotImplementedError("RemoteGraph loading will be implemented in PR 1.3")

def reverse(self) -> 'ASTRemoteGraph':
raise NotImplementedError("RemoteGraph reversal not supported")


class ASTRef(ASTObject):
"""Execute a chain with reference to a DAG binding"""
def __init__(self, ref: str, chain: List[ASTObject]):
super().__init__()
self.ref = ref
self.chain = chain

def validate(self, collect_all: bool = False) -> Optional[List['GFQLValidationError']]:
assert isinstance(self.ref, str), "ref must be a string"
assert len(self.ref) > 0, "ref cannot be empty"
assert isinstance(self.chain, list), "chain must be a list"
for i, op in enumerate(self.chain):
assert isinstance(op, ASTObject), f"chain[{i}] must be ASTObject, got {type(op)}"
op.validate()
return None

def to_json(self, validate=True) -> dict:
if validate:
self.validate()
return {
'type': 'Ref',
'ref': self.ref,
'chain': [op.to_json() for op in self.chain]
}

@classmethod
def from_json(cls, d: dict, validate: bool = True) -> 'ASTRef':
assert 'ref' in d, "Ref missing ref"
assert 'chain' in d, "Ref missing chain"
out = cls(
ref=d['ref'],
chain=[from_json(op, validate=validate) for op in d['chain']]
)
if validate:
out.validate()
return out

def __call__(self, g: Plottable, prev_node_wavefront: Optional[DataFrameT],
target_wave_front: Optional[DataFrameT], engine: Engine) -> Plottable:
# Implementation in PR 1.2
raise NotImplementedError("Ref execution will be implemented in PR 1.2")

def reverse(self) -> 'ASTRef':
# Reverse the chain operations
return ASTRef(self.ref, [op.reverse() for op in reversed(self.chain)])


###

def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge]:
def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge, ASTLet, ASTRemoteGraph, ASTRef]:
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' or 'Edge'"
ErrorCode.E105, "AST JSON missing required 'type' field", suggestion="Add 'type' field: 'Node', 'Edge', 'QueryDAG', 'RemoteGraph', or 'Ref'"
)

out: Union[ASTNode, ASTEdge]
out: Union[ASTNode, ASTEdge, ASTLet, ASTRemoteGraph, ASTRef]
if o['type'] == 'Node':
out = ASTNode.from_json(o, validate=validate)
elif o['type'] == 'Edge':
Expand All @@ -680,12 +776,27 @@ def from_json(o: JSONVal, validate: bool = True) -> Union[ASTNode, ASTEdge]:
"Edge missing required 'direction' field",
suggestion="Add 'direction' field: 'forward', 'reverse', or 'undirected'",
)
elif o['type'] == 'QueryDAG' or o['type'] == 'Let':
# Support both types for backward compatibility
out = ASTLet.from_json(o, validate=validate)
elif o['type'] == 'RemoteGraph':
out = ASTRemoteGraph.from_json(o, validate=validate)
elif o['type'] == 'Ref':
out = ASTRef.from_json(o, validate=validate)
else:
raise GFQLSyntaxError(
ErrorCode.E101,
f"Unknown AST type: {o['type']}",
field="type",
value=o["type"],
suggestion="Use 'Node' or 'Edge'",
suggestion="Use 'Node', 'Edge', 'Let', 'RemoteGraph', or 'Ref'",
)
return out


###############################################################################
# User-friendly aliases for public API

let = ASTLet # noqa: E305
remote = ASTRemoteGraph # noqa: E305
ref = ASTRef # noqa: E305
3 changes: 1 addition & 2 deletions graphistry/compute/ast_temporal.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import Any, Dict, Union
from typing import Any, Dict
from abc import ABC, abstractmethod
from datetime import date, datetime, time
from dateutil import parser as date_parser # type: ignore[import]
import pandas as pd
import pytz # type: ignore[import]

from graphistry.models.gfql.types.temporal import DateTimeWire, DateWire, TimeWire, TemporalWire
from graphistry.utils.json import JSONVal


class TemporalValue(ABC):
Expand Down
2 changes: 1 addition & 1 deletion graphistry/compute/chain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Dict, Union, cast, List, Tuple, Sequence, Optional, TYPE_CHECKING
from typing import Dict, Union, cast, List, Tuple, Optional, TYPE_CHECKING
from graphistry.Engine import Engine, EngineAbstract, df_concat, resolve_engine

from graphistry.Plottable import Plottable
Expand Down
Loading
Loading