Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a534752
feat(gfql): Enable Let bindings to accept matchers (ASTNode/ASTEdge)
lmeyerov Sep 26, 2025
a8c638d
fix(gfql): Fix lint errors and test failures in Let matchers implemen…
lmeyerov Sep 26, 2025
59c4af0
test: Skip parallel_independent_branches test due to query parameter …
lmeyerov Sep 26, 2025
9cb9950
test: Add deprecation warning suppression for chain tests
lmeyerov Sep 27, 2025
b91bafb
test: Update validation tests to reflect that ASTNode/ASTEdge are now…
lmeyerov Sep 27, 2025
8dbbc99
test: Fix validation test assertions for new error message format
lmeyerov Sep 27, 2025
7a6cbbc
test: Fix ASTCall test expectations for filtered DAG behavior
lmeyerov Sep 27, 2025
e812ac0
refactor(gfql): Improve type hint for dict parameter in ASTLet.__init__
lmeyerov Sep 27, 2025
a723f40
fix(gfql): Fix test_chain_let_output_selection by correcting DAG exec…
lmeyerov Sep 28, 2025
0419caa
Revert "fix(gfql): Fix test_chain_let_output_selection by correcting …
lmeyerov Sep 28, 2025
bd51708
fix(gfql): Enable independent Chain filtering in DAG context
lmeyerov Sep 28, 2025
3942730
fix(gfql): Exclude internal bindings from error messages
lmeyerov Sep 28, 2025
cdaccd9
fix(gfql): Fix ASTRef to return filtered results instead of marking
lmeyerov Sep 28, 2025
1ca16f6
test: Fix tests for ASTRef filtering semantics
lmeyerov Sep 28, 2025
0f4b9b7
fix(lint): Fix flake8 E712 comparison to True
lmeyerov Sep 28, 2025
edef0c1
fix(mypy): Fix JSONVal cyclic type and deprecated import issues
lmeyerov Sep 28, 2025
12a12cf
test: Fix test_edge_with_name test for Chain edge name handling
lmeyerov Sep 28, 2025
53e2c8b
build: Add host-level convenience scripts for development tools
lmeyerov Sep 29, 2025
22efb11
fix(gfql): Implement filter semantics for Let bindings with matchers
lmeyerov Sep 29, 2025
84b489d
fix: Ensure ASTNode/ASTEdge use original graph in Let bindings
lmeyerov Sep 29, 2025
74b408d
docs: Update CHANGELOG for Let matchers and dev scripts
lmeyerov Sep 29, 2025
28ebab8
Merge master into feat/let-matchers-support
lmeyerov Sep 29, 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
* GFQL: Fix hypergraph typing - add method to Plottable Protocol, resolve circular import

### Added
* GFQL: Let bindings now accept ASTNode/ASTEdge matchers directly (#751)
* Direct syntax: `let({'persons': n({'type': 'person'})})` without Chain wrapper
* Auto-converts list syntax to Chain for backward compatibility
* **Important**: Uses FILTER semantics - `n()` returns nodes only (no edges)
* Independent bindings operate on root graph unless using `ref()`
* Development: Host-level convenience scripts for local testing
* `./bin/pytest.sh` - Runs tests with highest available Python (3.8-3.14)
* `./bin/mypy.sh` - Type checking without Docker overhead
* `./bin/flake8.sh` - Linting with auto-detection of Python version
* GFQL: Add hypergraph transformation support for creating entity relationships from event data
* Simple transformation: `g.gfql(hypergraph(entity_types=['user', 'product']))`
* Typed builder with IDE support: `from graphistry.compute import hypergraph`
Expand Down
65 changes: 65 additions & 0 deletions bin/flake8.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash
set -e

# Try to find the best Python version available
PYTHON_BIN=""
for ver in 3.14 3.13 3.12 3.11 3.10 3.9 3.8; do
if command -v python$ver &> /dev/null; then
PYTHON_BIN=python$ver
break
fi
done

if [ -z "$PYTHON_BIN" ]; then
echo "No suitable Python version found (3.8-3.14)"
exit 1
fi

echo "Using Python: $PYTHON_BIN"
$PYTHON_BIN --version

# Install flake8 if not available
if ! $PYTHON_BIN -m flake8 --version &> /dev/null; then
echo "Installing flake8..."
$PYTHON_BIN -m pip install flake8 --user
fi

# Get the script directory and repo root
SCRIPT_DIR="$( cd "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# Change to repo root
cd "$REPO_ROOT"

# Check if specific files were passed as arguments
if [ $# -eq 0 ]; then
# No arguments, run on entire graphistry directory
TARGET="graphistry"
else
# Use provided arguments
TARGET="$@"
fi

echo "Running flake8 on: $TARGET"

# Quick syntax error check
echo "=== Running quick syntax check ==="
$PYTHON_BIN -m flake8 \
$TARGET \
--count \
--select=E9,F63,F7,F82 \
--show-source \
--statistics

# Full lint check
echo "=== Running full lint check ==="
$PYTHON_BIN -m flake8 \
$TARGET \
--exclude=graphistry/graph_vector_pb2.py,graphistry/_version.py \
--count \
--ignore=C901,E121,E122,E123,E124,E125,E128,E131,E144,E201,E202,E203,E231,E251,E265,E301,E302,E303,E401,E501,E722,F401,W291,W293,W503 \
--max-complexity=10 \
--max-line-length=127 \
--statistics

echo "Flake8 check completed successfully!"
64 changes: 64 additions & 0 deletions bin/mypy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
set -e

# Try to find the best Python version available
PYTHON_BIN=""
for ver in 3.14 3.13 3.12 3.11 3.10 3.9 3.8; do
if command -v python$ver &> /dev/null; then
PYTHON_BIN=python$ver
break
fi
done

if [ -z "$PYTHON_BIN" ]; then
echo "No suitable Python version found (3.8-3.14)"
exit 1
fi

echo "Using Python: $PYTHON_BIN"
$PYTHON_BIN --version

# Install mypy if not available
if ! $PYTHON_BIN -m mypy --version &> /dev/null; then
echo "Installing mypy..."
$PYTHON_BIN -m pip install mypy --user
fi

# Get the script directory and repo root
SCRIPT_DIR="$( cd "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# Change to repo root
cd "$REPO_ROOT"

# Check if specific files were passed as arguments
if [ $# -eq 0 ]; then
# No arguments, run on entire graphistry directory using config file
TARGET=""
CONFIG_ARG="--config-file mypy.ini"
else
# Use provided arguments
TARGET="$@"
# Still use config file for consistency
CONFIG_ARG="--config-file mypy.ini"
fi

echo "Running mypy..."
if [ -z "$TARGET" ]; then
echo "Checking entire graphistry directory with mypy.ini config"
else
echo "Checking: $TARGET"
fi

# Show mypy version
$PYTHON_BIN -m mypy --version

# Run mypy with config file
# If no target specified, mypy will use the paths defined in mypy.ini
if [ -z "$TARGET" ]; then
$PYTHON_BIN -m mypy $CONFIG_ARG graphistry
else
$PYTHON_BIN -m mypy $CONFIG_ARG $TARGET
fi

echo "Mypy check completed!"
55 changes: 55 additions & 0 deletions bin/pytest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
set -e

# Try to find the best Python version available
PYTHON_BIN=""
for ver in 3.14 3.13 3.12 3.11 3.10 3.9 3.8; do
if command -v python$ver &> /dev/null; then
PYTHON_BIN=python$ver
break
fi
done

if [ -z "$PYTHON_BIN" ]; then
echo "No suitable Python version found (3.8-3.14)"
exit 1
fi

echo "Using Python: $PYTHON_BIN"
$PYTHON_BIN --version

# Install pytest if not available
if ! $PYTHON_BIN -m pytest --version &> /dev/null; then
echo "Installing pytest..."
$PYTHON_BIN -m pip install pytest pytest-xdist --user
fi

# Get the script directory and repo root
SCRIPT_DIR="$( cd "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# Change to repo root
cd "$REPO_ROOT"

# Show pytest version
$PYTHON_BIN -m pytest --version

# Default pytest arguments for verbosity and parallel execution
PYTEST_ARGS="-vv"

# Check if any arguments were passed
if [ $# -eq 0 ]; then
# No arguments, run default test location
TARGET="graphistry/tests"
else
# Use provided arguments
TARGET="$@"
fi

echo "Running pytest on: $TARGET"

# Run pytest with verbose output and any additional arguments
# Using python -B to not write .pyc files
$PYTHON_BIN -B -m pytest $PYTEST_ARGS $TARGET

echo "Pytest completed!"
10 changes: 8 additions & 2 deletions graphistry/client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Any, Optional, Literal, cast, Protocol, TypedDict, Dict, MutableMapping, Type, TypeVar, Union, overload, Iterator
from functools import lru_cache
import json
from typing_extensions import deprecated
import warnings

from graphistry.privacy import Privacy
from . import util
Expand Down Expand Up @@ -146,8 +146,14 @@ def api_token(self, value: Optional[str] = None) -> Optional[str]:
...


@deprecated("Use the session pattern instead")
def use_global_session() -> ClientSession:
"""Use the session pattern instead.

.. deprecated::
This function is deprecated. Use the session pattern instead.
"""
warnings.warn("use_global_session() is deprecated. Use the session pattern instead.",
DeprecationWarning, stacklevel=2)
from .pygraphistry import PyGraphistry
return PyGraphistry.session

Expand Down
44 changes: 14 additions & 30 deletions graphistry/compute/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,19 +663,21 @@ class ASTLet(ASTObject):
:raises GFQLTypeError: If bindings is not a dict or contains invalid keys/values

**Example::**


# Matchers now supported directly (operate on root graph)
dag = ASTLet({
'persons': n({'type': 'person'}),
'friends': ASTRef('persons', [e_forward({'rel': 'friend'})])
})
"""
bindings: Dict[str, Union['ASTObject', 'Chain', Plottable]]

def __init__(self, bindings: Dict[str, Union['ASTObject', 'Chain', Plottable, dict]], validate: bool = True) -> None:
def __init__(self, bindings: Dict[str, Union['ASTObject', 'Chain', Plottable, Dict[str, Any]]], validate: bool = True) -> None:
"""Initialize Let with named bindings.

:param bindings: Dictionary mapping names to GraphOperation instances or JSON dicts
:type bindings: Dict[str, Union[ASTObject, Chain, Plottable, dict]]

:param bindings: Dictionary mapping names to GraphOperation instances or JSON dicts.
JSON dicts must have a 'type' field indicating the AST object type.
:type bindings: Dict[str, Union[ASTObject, Chain, Plottable, Dict[str, Any]]]
:param validate: Whether to validate the bindings immediately
:type validate: bool
"""
Expand All @@ -691,17 +693,7 @@ def __init__(self, bindings: Dict[str, Union['ASTObject', 'Chain', Plottable, di

obj_type = value.get('type')
# Check if it's a valid GraphOperation type
if obj_type in ['Node', 'Edge']:
# These are wavefront matchers, not allowed
from graphistry.compute.exceptions import ErrorCode, GFQLTypeError
raise GFQLTypeError(
ErrorCode.E201,
f"binding value cannot be {obj_type} (wavefront matcher)",
field=f"bindings.{name}",
value=obj_type,
suggestion="Use operations that produce Plottable objects like Chain, Ref, Call, RemoteGraph, or Let"
)
elif obj_type == 'Chain':
if obj_type == 'Chain':
# Import and convert Chain
from graphistry.compute.chain import Chain
chain_obj = Chain.from_json(value, validate=False)
Expand Down Expand Up @@ -742,24 +734,16 @@ def _validate_fields(self) -> None:
# Check if value is a valid GraphOperation type
# Import here to avoid circular imports
from graphistry.compute.chain import Chain # noqa: F402

# GraphOperation includes specific AST types that produce Plottable objects
# Excludes ASTNode/ASTEdge which are wavefront matchers
if isinstance(v, (ASTNode, ASTEdge)):
raise GFQLTypeError(
ErrorCode.E201,
f"binding value cannot be {type(v).__name__} (wavefront matcher)",
field=f"bindings.{k}",
value=type(v).__name__,
suggestion="Use operations that produce Plottable objects like ASTRef, ASTCall, ASTRemoteGraph, ASTLet, Chain, or Plottable instances"
)
elif not isinstance(v, (ASTRef, ASTCall, ASTRemoteGraph, ASTLet, Plottable, Chain)):

# GraphOperation now includes all AST types
# ASTNode/ASTEdge are now allowed and will operate on the root graph
if not isinstance(v, (ASTNode, ASTEdge, ASTRef, ASTCall, ASTRemoteGraph, ASTLet, Plottable, Chain)):
raise GFQLTypeError(
ErrorCode.E201,
"binding value must be a GraphOperation (Plottable, Chain, ASTRef, ASTCall, ASTRemoteGraph, or ASTLet)",
"binding value must be a valid operation (ASTNode, ASTEdge, Chain, ASTRef, ASTCall, ASTRemoteGraph, ASTLet, or Plottable)",
field=f"bindings.{k}",
value=type(v).__name__,
suggestion="Use operations that produce Plottable objects, not wavefront matchers"
suggestion="Use a valid graph operation or matcher"
)
# TODO: Check for cycles in DAG
return None
Expand Down
3 changes: 2 additions & 1 deletion graphistry/compute/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ def combine_steps(g: Plottable, kind: str, steps: List[Tuple[ASTObject,Plottable
on=id,
how='left'
)
out_df[op._name] = out_df[op._name].fillna(False).astype(bool)
s = out_df[op._name]
out_df[op._name] = s.where(s.notna(), False).astype('bool')
out_df = out_df.merge(getattr(g, df_fld), on=id, how='left')

logger.debug('COMBINED[%s] >>\n%s', kind, out_df)
Expand Down
Loading
Loading