diff --git a/CHANGELOG.md b/CHANGELOG.md index ac3c953fa8..158ab27724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm * Support for datetime, date, and time comparisons with operators: `gt`, `lt`, `ge`, `le`, `eq`, `ne`, `between`, `is_in` * Proper timezone handling for datetime comparisons * Type-safe temporal value handling with TypeGuard annotations + * Temporal value classes: `DateTimeValue`, `DateValue`, `TimeValue` for explicit temporal types + * Wire protocol support for JSON serialization of temporal predicates + * Comprehensive documentation: datetime filtering guide, wire protocol reference, and examples notebook * CI: Enable notebook validation by default in docs builds (set VALIDATE_NOTEBOOK_EXECUTION=0 to disable) * CI: Run notebook validation after doc generation for faster error detection diff --git a/demos/gfql/temporal_predicates.ipynb b/demos/gfql/temporal_predicates.ipynb new file mode 100644 index 0000000000..877306dd09 --- /dev/null +++ b/demos/gfql/temporal_predicates.ipynb @@ -0,0 +1,450 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# GFQL DateTime Filtering Examples\n\nThis notebook shows how to filter graph data by dates and times using GFQL predicates." + }, + { + "cell_type": "markdown", + "source": "## Table of Contents\n\n**Key Temporal Filtering Concepts:**\n\n1. **Basic DateTime Filtering** - Filter by specific dates and times\n2. **Date-Only Filtering** - Ignore time components \n3. **Time-of-Day Filtering** - Filter by time patterns\n4. **Complex Temporal Queries** - Combine with other predicates\n5. **Temporal Value Classes** - Explicit temporal objects\n6. **Timezone-Aware Filtering** - Handle timezone conversions\n7. **Chain Operations** - Multi-hop temporal queries\n8. **Wire Protocol Dicts** - JSON-compatible configuration\n\n**Quick Reference:**\n- `gt()`, `lt()`, `ge()`, `le()` - Greater/less than comparisons\n- `between()` - Range queries\n- `is_in()` - Match specific values\n- `DateTimeValue`, `DateValue`, `TimeValue` - Explicit temporal types", + "metadata": {} + }, + { + "cell_type": "code", + "metadata": {}, + "outputs": [], + "source": "# Standard Python datetime imports\nimport pandas as pd\nimport numpy as np\nfrom datetime import datetime, date, time, timedelta\nimport pytz\n\n# Graphistry imports\nimport graphistry\nfrom graphistry import n, e_forward, e_reverse, e_undirected\n\n# Temporal predicates\nfrom graphistry.compute import (\n gt, lt, ge, le, eq, ne, between, is_in,\n DateTimeValue, DateValue, TimeValue\n)", + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup: Create Sample Data\n", + "\n", + "Let's create a sample dataset representing a transaction network with temporal data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate sample transaction data\n", + "np.random.seed(42)\n", + "\n", + "# Create nodes (accounts)\n", + "n_accounts = 100\n", + "accounts_df = pd.DataFrame({\n", + " 'account_id': [f'ACC_{i:04d}' for i in range(n_accounts)],\n", + " 'account_type': np.random.choice(['checking', 'savings', 'business'], n_accounts),\n", + " 'created_date': pd.date_range('2020-01-01', periods=n_accounts, freq='W'),\n", + " 'last_active': pd.date_range('2023-01-01', periods=n_accounts, freq='D') + \n", + " pd.to_timedelta(np.random.randint(0, 365, n_accounts), unit='D')\n", + "})\n", + "\n", + "# Create edges (transactions)\n", + "n_transactions = 500\n", + "transactions_df = pd.DataFrame({\n", + " 'transaction_id': [f'TXN_{i:06d}' for i in range(n_transactions)],\n", + " 'source': np.random.choice(accounts_df['account_id'], n_transactions),\n", + " 'target': np.random.choice(accounts_df['account_id'], n_transactions),\n", + " 'amount': np.random.exponential(100, n_transactions).round(2),\n", + " 'timestamp': pd.date_range('2023-01-01', periods=n_transactions, freq='H') + \n", + " pd.to_timedelta(np.random.randint(0, 8760, n_transactions), unit='H'),\n", + " 'transaction_time': [time(np.random.randint(0, 24), np.random.randint(0, 60)) \n", + " for _ in range(n_transactions)],\n", + " 'transaction_type': np.random.choice(['transfer', 'payment', 'deposit'], n_transactions)\n", + "})\n", + "\n", + "print(f\"Created {len(accounts_df)} accounts and {len(transactions_df)} transactions\")\n", + "print(f\"\\nTransaction date range: {transactions_df['timestamp'].min()} to {transactions_df['timestamp'].max()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Create graphistry instance\n", + "g = graphistry.edges(transactions_df, 'source', 'target').nodes(accounts_df, 'account_id')\n", + "print(f\"Graph: {len(g._nodes)} nodes, {len(g._edges)} edges\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Basic DateTime Filtering\n", + "\n", + "Filter transactions based on datetime values using edge predicates." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter transactions after a specific date\n", + "# First, filter the edges directly\n", + "cutoff_date = datetime(2023, 7, 1)\n", + "recent_edges = g._edges[gt(pd.Timestamp(cutoff_date))(g._edges['timestamp'])]\n", + "recent_g = g.edges(recent_edges)\n", + "\n", + "print(f\"Transactions after {cutoff_date}: {len(recent_g._edges)}\")\n", + "recent_g._edges[['transaction_id', 'timestamp', 'amount']].head()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Alternative: Use chain with edge operations\n", + "# Start from all nodes, then follow edges with temporal filter\n", + "recent_chain = g.chain([\n", + " n(), # Start with all nodes\n", + " e_forward({\n", + " \"timestamp\": gt(pd.Timestamp(cutoff_date))\n", + " })\n", + "])\n", + "\n", + "print(f\"Transactions after {cutoff_date} (chain): {len(recent_chain._edges)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter transactions in a specific month\n", + "march_edges = g._edges[\n", + " between(\n", + " datetime(2023, 3, 1),\n", + " datetime(2023, 3, 31, 23, 59, 59)\n", + " )(g._edges['timestamp'])\n", + "]\n", + "march_g = g.edges(march_edges)\n", + "\n", + "print(f\"Transactions in March 2023: {len(march_g._edges)}\")\n", + "march_g._edges[['transaction_id', 'timestamp', 'amount']].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Date-Only Filtering\n", + "\n", + "Filter nodes based on dates, ignoring time components." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter accounts created after a specific date\n", + "new_accounts = g.chain([\n", + " n(filter_dict={\n", + " \"created_date\": ge(date(2021, 1, 1))\n", + " })\n", + "])\n", + "\n", + "print(f\"Accounts created after 2021: {len(new_accounts._nodes)}\")\n", + "new_accounts._nodes[['account_id', 'created_date', 'account_type']].head()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Find accounts active in the last 90 days\n", + "ninety_days_ago = datetime.now().date() - timedelta(days=90)\n", + "active_accounts = g.chain([\n", + " n(filter_dict={\n", + " \"last_active\": gt(pd.Timestamp(ninety_days_ago))\n", + " })\n", + "])\n", + "\n", + "print(f\"Recently active accounts: {len(active_accounts._nodes)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Time-of-Day Filtering\n", + "\n", + "Filter transactions based on time of day." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Find transactions during business hours (9 AM - 5 PM)\n", + "business_hours_edges = g._edges[\n", + " between(\n", + " time(9, 0, 0),\n", + " time(17, 0, 0)\n", + " )(g._edges['transaction_time'])\n", + "]\n", + "business_hours_g = g.edges(business_hours_edges)\n", + "\n", + "print(f\"Business hour transactions: {len(business_hours_g._edges)}\")\n", + "print(f\"Percentage of total: {len(business_hours_g._edges) / len(g._edges) * 100:.1f}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Find transactions at specific times (e.g., on the hour)\n", + "on_the_hour_times = [time(h, 0, 0) for h in range(24)]\n", + "on_hour_edges = g._edges[\n", + " is_in(on_the_hour_times)(g._edges['transaction_time'])\n", + "]\n", + "on_hour_g = g.edges(on_hour_edges)\n", + "\n", + "print(f\"Transactions on the hour: {len(on_hour_g._edges)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Complex Temporal Queries\n", + "\n", + "Combine temporal predicates with other filters for complex queries." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# Find large transactions (>$500) in Q4 2023\n", + "q4_mask = between(\n", + " datetime(2023, 10, 1),\n", + " datetime(2023, 12, 31, 23, 59, 59)\n", + ")(g._edges['timestamp'])\n", + "large_mask = gt(500)(g._edges['amount'])\n", + "\n", + "q4_large_edges = g._edges[q4_mask & large_mask]\n", + "q4_large_g = g.edges(q4_large_edges)\n", + "\n", + "print(f\"Large Q4 2023 transactions: {len(q4_large_g._edges)}\")\n", + "if len(q4_large_g._edges) > 0:\n", + " print(f\"Total value: ${q4_large_g._edges['amount'].sum():,.2f}\")\n", + " print(f\"Average: ${q4_large_g._edges['amount'].mean():,.2f}\")" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "outputs": [], + "source": "# Multi-hop query: Find accounts that received money recently\n# and then sent money to business accounts\nthirty_days_ago = datetime.now() - timedelta(days=30)\n\n# First, find recent transactions\nrecent_edges = g._edges[gt(pd.Timestamp(thirty_days_ago))(g._edges['timestamp'])]\nrecent_g = g.edges(recent_edges)\n\n# Use chain to find money flow pattern\nmoney_flow = recent_g.chain([\n # Start with any node\n n(),\n # Follow incoming edges (as destination)\n e_reverse(),\n # Go to source nodes\n n(),\n # Follow outgoing edges\n e_forward(),\n # To business accounts\n n(filter_dict={\"account_type\": \"business\"})\n])\n\nprint(f\"Money flow pattern found: {len(money_flow._nodes)} business accounts\")", + "execution_count": 12 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Using Temporal Value Classes\n", + "\n", + "Use explicit temporal value classes for more control." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Create temporal values with specific properties\n", + "dt_value = DateTimeValue(\"2023-06-15T14:30:00\", \"UTC\")\n", + "date_value = DateValue(\"2023-06-15\")\n", + "time_value = TimeValue(\"14:30:00\")\n", + "\n", + "# Use in predicates\n", + "specific_edges = g._edges[gt(dt_value)(g._edges['timestamp'])]\n", + "specific_g = g.edges(specific_edges)\n", + "\n", + "print(f\"Transactions after {dt_value.value}: {len(specific_g._edges)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Timezone-Aware Filtering\n", + "\n", + "Handle timezone-aware datetime comparisons." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "# Add timezone info to our data for this example\n", + "transactions_df_tz = transactions_df.copy()\n", + "transactions_df_tz['timestamp_utc'] = pd.to_datetime(transactions_df_tz['timestamp']).dt.tz_localize('UTC')\n", + "transactions_df_tz['timestamp_eastern'] = transactions_df_tz['timestamp_utc'].dt.tz_convert('US/Eastern')\n", + "\n", + "g_tz = graphistry.edges(transactions_df_tz, 'source', 'target')\n", + "\n", + "# Filter using Eastern time\n", + "eastern = pytz.timezone('US/Eastern')\n", + "eastern_cutoff = eastern.localize(datetime(2023, 7, 1, 9, 0, 0)) # 9 AM Eastern\n", + "\n", + "eastern_morning_edges = g_tz._edges[\n", + " gt(pd.Timestamp(eastern_cutoff))(g_tz._edges['timestamp_eastern'])\n", + "]\n", + "eastern_morning_g = g_tz.edges(eastern_morning_edges)\n", + "\n", + "print(f\"Transactions after 9 AM Eastern on July 1, 2023: {len(eastern_morning_g._edges)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Chain Operations with Temporal Edge Filters\n", + "\n", + "Demonstrate using temporal predicates in chain operations with proper edge filtering." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# Find paths through recent high-value transactions\n", + "recent_high_value = g.chain([\n", + " # Start from all nodes\n", + " n(),\n", + " # Follow edges with temporal and amount filters\n", + " e_forward({\n", + " \"timestamp\": gt(datetime.now() - timedelta(days=7)),\n", + " \"amount\": gt(200)\n", + " }),\n", + " # Reach destination nodes\n", + " n()\n", + "])\n", + "\n", + "print(f\"Recent high-value transaction paths:\")\n", + "print(f\" Nodes: {len(recent_high_value._nodes)}\")\n", + "print(f\" Edges: {len(recent_high_value._edges)}\")" + ] + }, + { + "cell_type": "code", + "source": "# Wire protocol dicts in is_in predicates\n# Useful for checking against multiple specific timestamps\n\nimportant_dates = [\n {\"type\": \"datetime\", \"value\": \"2023-01-01T00:00:00\", \"timezone\": \"UTC\"}, # New Year\n {\"type\": \"datetime\", \"value\": \"2023-07-04T00:00:00\", \"timezone\": \"UTC\"}, # July 4th\n {\"type\": \"datetime\", \"value\": \"2023-12-25T00:00:00\", \"timezone\": \"UTC\"}, # Christmas\n]\n\n# Note: This checks for exact timestamp matches\n# For date matching, you'd need to extract the date portion\nholiday_pred = is_in(important_dates)\n\n# For demonstration, let's check if any transactions happened exactly at midnight on these days\n# (In real data, you'd probably want to check date ranges instead)\nprint(f\"Checking for transactions at midnight on holidays...\")\nprint(f\"(This is likely 0 unless transactions were specifically created at midnight)\")", + "metadata": {}, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## Summary\n\nThis notebook demonstrated:\n\n1. **DateTime filtering** with `gt`, `lt`, `between` predicates on edges\n2. **Date-only filtering** for day-level granularity on nodes\n3. **Time-of-day filtering** for patterns like business hours\n4. **Complex queries** combining temporal and non-temporal predicates\n5. **Multi-hop queries** with temporal constraints using chain operations\n6. **Temporal value classes** for explicit control\n7. **Timezone-aware** filtering\n8. **Wire protocol dictionaries** for JSON-compatible predicate configuration\n9. **Proper chain syntax** with edge filters in `e_forward()` and node filters in `n()`\n\nKey takeaways:\n- Temporal predicates work seamlessly with pandas datetime types\n- Wire protocol dicts enable configuration-driven filtering: `gt({\"type\": \"datetime\", \"value\": \"2023-01-01T00:00:00\", \"timezone\": \"UTC\"})`\n- Timezone awareness is built-in for accurate cross-timezone comparisons\n- Complex temporal patterns can be expressed through chain operations\n\nTemporal predicates in GFQL provide a powerful way to analyze time-series aspects of graph data, enabling complex temporal queries while maintaining the expressiveness of graph traversals.", + "metadata": {} + }, + { + "cell_type": "code", + "source": "# Build predicates programmatically with wire protocol dicts\ndef create_date_filter(year, month, day, comparison=\"gt\"):\n \"\"\"Create a date filter using wire protocol format\"\"\"\n date_dict = {\n \"type\": \"date\",\n \"value\": f\"{year:04d}-{month:02d}-{day:02d}\"\n }\n \n if comparison == \"gt\":\n return gt(date_dict)\n elif comparison == \"lt\":\n return lt(date_dict)\n elif comparison == \"ge\":\n return ge(date_dict)\n elif comparison == \"le\":\n return le(date_dict)\n else:\n raise ValueError(f\"Unknown comparison: {comparison}\")\n\n# Use the programmatic filter\nfilter_2023 = create_date_filter(2023, 1, 1, \"ge\")\naccounts_2023 = g.chain([\n n(filter_dict={\n \"created_date\": filter_2023\n })\n])\n\nprint(f\"Accounts created in 2023 or later: {len(accounts_2023._nodes)}\")", + "metadata": {}, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": "# Example: Load predicate configuration from JSON\nimport json\n\n# Simulate loading from a JSON config file\nconfig_json = '''\n{\n \"filters\": {\n \"recent_transactions\": {\n \"timestamp\": {\n \"type\": \"gt\",\n \"value\": {\n \"type\": \"datetime\",\n \"value\": \"2023-10-01T00:00:00\",\n \"timezone\": \"UTC\"\n }\n }\n },\n \"business_hours\": {\n \"transaction_time\": {\n \"type\": \"between\",\n \"start\": {\"type\": \"time\", \"value\": \"09:00:00\"},\n \"end\": {\"type\": \"time\", \"value\": \"17:00:00\"}\n }\n }\n }\n}\n'''\n\nconfig = json.loads(config_json)\n\n# Use the wire protocol dict directly\nrecent_filter = config[\"filters\"][\"recent_transactions\"][\"timestamp\"][\"value\"]\nrecent_edges = g._edges[gt(recent_filter)(g._edges['timestamp'])]\nrecent_g = g.edges(recent_edges)\n\nprint(f\"Recent transactions (from JSON config): {len(recent_g._edges)}\")", + "metadata": {}, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": "# Wire protocol dictionaries work directly in Python\n# These are equivalent:\npred1 = gt(pd.Timestamp(\"2023-07-01\"))\npred2 = gt({\"type\": \"datetime\", \"value\": \"2023-07-01T00:00:00\", \"timezone\": \"UTC\"})\n\n# Test they produce the same results\nresult1 = pred1(g._edges['timestamp'])\nresult2 = pred2(g._edges['timestamp'])\nprint(f\"Results are identical: {result1.equals(result2)}\")\nprint(f\"Transactions after July 1, 2023: {result1.sum()}\")", + "metadata": {}, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": "## 8. Using Wire Protocol Dictionaries\n\nYou can pass wire protocol dictionaries directly to temporal predicates. This is useful for:\n- Loading predicate configurations from JSON files\n- Building predicates programmatically\n- Sharing predicate definitions between systems", + "metadata": {} + }, + { + "cell_type": "markdown", + "source": "## What's Next?\n\n- **[Datetime Filtering Guide](../../gfql/datetime_filtering.html)** - Full temporal predicate reference\n- **[Wire Protocol Reference](../../gfql/wire_protocol_examples.html)** - JSON serialization examples\n- **[GFQL Documentation](../../gfql/index.html)** - Complete GFQL reference", + "metadata": {} + }, + { + "cell_type": "code", + "metadata": {}, + "outputs": [], + "source": "# Complex multi-hop with temporal constraints\n# Find 2-hop paths through recent transactions\ntwo_hop_recent = g.chain([\n # Start from business accounts\n n(filter_dict={\"account_type\": \"business\"}),\n # First hop: recent outgoing transactions\n e_forward({\n \"timestamp\": gt(datetime.now() - timedelta(days=30))\n }, name=\"hop1\"),\n # Intermediate nodes\n n(),\n # Second hop: any transaction\n e_forward(name=\"hop2\"),\n # Final nodes\n n()\n])\n\nprint(f\"2-hop paths from business accounts through recent transactions:\")\nprint(f\" Total edges: {len(two_hop_recent._edges)}\")\nprint(f\" Hop 1 edges: {two_hop_recent._edges['hop1'].sum()}\")\nprint(f\" Hop 2 edges: {two_hop_recent._edges['hop2'].sum()}\")", + "execution_count": 16 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This notebook demonstrated:\n", + "\n", + "1. **DateTime filtering** with `gt`, `lt`, `between` predicates on edges\n", + "2. **Date-only filtering** for day-level granularity on nodes\n", + "3. **Time-of-day filtering** for patterns like business hours\n", + "4. **Complex queries** combining temporal and non-temporal predicates\n", + "5. **Multi-hop queries** with temporal constraints using chain operations\n", + "6. **Temporal value classes** for explicit control\n", + "7. **Timezone-aware** filtering\n", + "8. **Proper chain syntax** with edge filters in `e_forward()` and node filters in `n()`\n", + "\n", + "Temporal predicates in GFQL provide a powerful way to analyze time-series aspects of graph data, enabling complex temporal queries while maintaining the expressiveness of graph traversals." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/docs/docker/build-docs.sh b/docs/docker/build-docs.sh index 2168299e53..5ff97c342a 100755 --- a/docs/docker/build-docs.sh +++ b/docs/docker/build-docs.sh @@ -45,6 +45,7 @@ esac # Validate notebooks after building docs NOTEBOOKS_TO_VALIDATE=( "/docs/test_notebooks/test_graphistry_import.ipynb" + "/docs/source/demos/gfql/temporal_predicates.ipynb" ) for notebook in "${NOTEBOOKS_TO_VALIDATE[@]}"; do diff --git a/docs/source/api/gfql/predicates.rst b/docs/source/api/gfql/predicates.rst index fedacd84df..039d793edb 100644 --- a/docs/source/api/gfql/predicates.rst +++ b/docs/source/api/gfql/predicates.rst @@ -53,3 +53,12 @@ Temporal :undoc-members: :show-inheritance: :noindex: + +Temporal Values +--------------- + +.. automodule:: graphistry.compute.ast_temporal + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/docs/source/gfql/datetime_filtering.md b/docs/source/gfql/datetime_filtering.md new file mode 100644 index 0000000000..78990da77c --- /dev/null +++ b/docs/source/gfql/datetime_filtering.md @@ -0,0 +1,369 @@ +# Working with Dates and Times + +GFQL predicates support filtering by datetime, date, and time values. This guide covers common patterns and gotchas when working with temporal data. + +## Required Imports + +```python +# Core imports +import graphistry +from graphistry import n, e_forward, e_reverse, e_undirected + +# Temporal predicates +from graphistry.compute import ( + gt, lt, ge, le, eq, ne, between, is_in, + DateTimeValue, DateValue, TimeValue +) + +# Standard datetime types +import pandas as pd +from datetime import datetime, date, time, timedelta +import pytz # For timezone support +``` + +## Supported Types and Standards + +### Supported Python Types + +- [`pd.Timestamp`](https://pandas.pydata.org/docs/reference/api/pandas.Timestamp.html) - Pandas timestamp +- [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) - Python datetime +- [`date`](https://docs.python.org/3/library/datetime.html#datetime.date) - Date only (no time) +- [`time`](https://docs.python.org/3/library/datetime.html#datetime.time) - Time only (no date) +- Wire protocol dicts - For ISO strings and JSON compatibility + +```python +# Use datetime objects +gt(pd.Timestamp("2023-01-01 12:00:00")) +between(datetime(2023, 1, 1), datetime(2023, 12, 31)) + +# Wire protocol dicts accept ISO strings +gt({"type": "datetime", "value": "2023-01-01T00:00:00", "timezone": "UTC"}) + +# Raw strings raise ValueError +gt("2023-01-01") # ValueError +``` + +### Creating from Strings + +```python +# Timestamps +pd.Timestamp("2023-01-01T12:00:00Z") # UTC +pd.Timestamp("2023-01-01 12:00:00") # Naive + +# Date/Time objects +date.fromisoformat("2023-01-01") # date(2023, 1, 1) +time.fromisoformat("14:30:00") # time(14, 30, 0) +``` + +### Standards +- [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) datetime strings: `"2023-01-01T12:00:00Z"` +- [IANA timezone](https://www.iana.org/time-zones) names: `"US/Eastern"`, `"UTC"` + +### Wire Protocol Types +For JSON serialization and cross-system compatibility: +- **DateTimeWire**: `{"type": "datetime", "value": "ISO-8601-string", "timezone": "IANA-timezone"}` +- **DateWire**: `{"type": "date", "value": "YYYY-MM-DD"}` +- **TimeWire**: `{"type": "time", "value": "HH:MM:SS[.ffffff]"}` + +Note: The `timezone` field is optional for DateTimeWire and defaults to "UTC" if omitted. + +## Basic Usage + +### Datetime Filtering + +Filter nodes or edges based on datetime values: + +```python +import pandas as pd +from datetime import datetime +from graphistry import n +from graphistry.compute import gt, lt, between + +# Filter nodes created after a specific datetime +recent_nodes = g.chain([ + n(filter_dict={"created_at": gt(pd.Timestamp("2023-01-01 12:00:00"))}) +]) + +# Filter edges within a date range +date_range_edges = g.chain([ + n(edge_match={"timestamp": between( + datetime(2023, 1, 1), + datetime(2023, 12, 31) + )}) +]) +``` + +### Date-Only Filtering + +For date comparisons (ignoring time): + +```python +from datetime import date +from graphistry.compute import eq, ge + +# Filter nodes by exact date +specific_date = g.chain([ + n(filter_dict={"event_date": eq(date(2023, 6, 15))}) +]) + +# Filter nodes on or after a date +after_date = g.chain([ + n(filter_dict={"start_date": ge(date(2023, 1, 1))}) +]) +``` + +### Time-Only Filtering + +Filter based on time of day: + +```python +from datetime import time +from graphistry.compute import is_in, between + +# Filter events at specific times +morning_events = g.chain([ + n(filter_dict={"event_time": is_in([ + time(9, 0, 0), + time(9, 30, 0), + time(10, 0, 0) + ])}) +]) + +# Filter events in time range +business_hours = g.chain([ + n(filter_dict={"timestamp": between( + time(9, 0, 0), + time(17, 0, 0) + )}) +]) +``` + +## Timezone Support + +```python +import pytz + +# Timezone-aware filtering +eastern = pytz.timezone('US/Eastern') +tz_aware_filter = g.chain([ + n(filter_dict={ + "timestamp": gt(pd.Timestamp("2023-01-01 12:00:00", tz=eastern)) + }) +]) +``` + +Comparisons automatically handle timezone conversions. + +## Advanced Usage + +### Mixed Temporal and Non-Temporal Predicates + +Combine temporal predicates with other filters: + +```python +from graphistry.compute import gt, lt, eq + +# Complex filter with multiple conditions +complex_filter = g.chain([ + n(filter_dict={ + "created_at": gt(datetime(2023, 1, 1)), + "status": eq("active"), + "priority": gt(5) + }) +]) +``` + +### Using Wire Protocol Dictionaries Directly + +You can pass wire protocol dictionaries directly to predicates, which is useful for programmatic predicate creation or when working with JSON configurations: + +```python +# Pass wire protocol dictionaries directly +filter_with_dict = g.chain([ + n(filter_dict={"timestamp": gt({ + "type": "datetime", + "value": "2023-01-01T12:00:00", + "timezone": "UTC" + })}) +]) + +# Works with all temporal predicates +date_range_filter = g.chain([ + n(filter_dict={"event_date": between( + {"type": "date", "value": "2023-01-01"}, + {"type": "date", "value": "2023-12-31"} + )}) +]) + +# And with is_in for multiple values +time_filter = g.chain([ + n(filter_dict={"event_time": is_in([ + {"type": "time", "value": "09:00:00"}, + {"type": "time", "value": "12:00:00"}, + {"type": "time", "value": "17:00:00"} + ])}) +]) +``` + +This is the same format used by the wire protocol, making it easy to: +- Store predicate configurations in JSON files +- Build predicates programmatically from external data sources +- Share predicate definitions between Python and other systems + +### Temporal Predicates in Multi-Hop Queries + +Use temporal filters in complex graph traversals: + +```python +# Find all transactions after a date, then their related accounts +recent_transactions = g.chain([ + n(filter_dict={"type": eq("transaction"), + "date": gt(date(2023, 6, 1))}), + n(edge_match={"relationship": eq("involves")}), + n(filter_dict={"type": eq("account")}) +]) +``` + +## Temporal Value Classes + +PyGraphistry provides three temporal value classes for precise control: + +### DateTimeValue + +For full datetime with optional timezone: + +```python +from graphistry.compute import DateTimeValue, gt + +# Create datetime value with timezone +dt_value = DateTimeValue("2023-01-01T12:00:00", "US/Eastern") + +# Use in predicate +filter_dt = g.chain([ + n(filter_dict={"timestamp": gt(dt_value)}) +]) +``` + +### DateValue + +For date-only comparisons: + +```python +from graphistry.compute import DateValue, between + +# Create date values +start = DateValue("2023-01-01") +end = DateValue("2023-12-31") + +# Use in between predicate +year_filter = g.chain([ + n(filter_dict={"event_date": between(start, end)}) +]) +``` + +### TimeValue + +For time-of-day comparisons: + +```python +from graphistry.compute import TimeValue, is_in + +# Create time values +morning = TimeValue("09:00:00") +noon = TimeValue("12:00:00") + +# Filter by specific times +time_filter = g.chain([ + n(filter_dict={"daily_event": is_in([morning, noon])}) +]) +``` + +## Best Practices + +1. **Use Explicit Types**: Always use `pd.Timestamp`, `datetime`, `date`, or `time` objects instead of strings to avoid ambiguity. + +2. **Timezone Awareness**: When working with timestamps across timezones, always specify timezones explicitly. + +3. **Performance**: Temporal comparisons are optimized for pandas DataFrames. For large datasets, ensure your datetime columns are properly typed. + +4. **JSON Serialization**: When serializing queries, temporal values are automatically converted to tagged dictionaries that preserve type and timezone information. + +## Unsupported Features + +### Duration/Interval Support +Currently, PyGraphistry does not support duration or interval types (e.g., ISO 8601 durations like "P1D" or "PT2H"). For duration-based queries: + +```python +# Instead of duration literals, calculate explicit timestamps +from datetime import datetime, timedelta + +# Find events within last 7 days +now = datetime.now() +week_ago = now - timedelta(days=7) +recent_events = g.chain([ + n(filter_dict={"timestamp": gt(pd.Timestamp(week_ago))}) +]) + +# For recurring intervals, use multiple conditions +business_days = g.chain([ + n(filter_dict={ + "timestamp": between( + pd.Timestamp("2023-01-01"), + pd.Timestamp("2023-12-31") + ) + }) +]) +``` + +## Common Patterns + +### Filter Recent Data + +```python +from datetime import datetime, timedelta + +# Get data from last 30 days +thirty_days_ago = datetime.now() - timedelta(days=30) +recent_data = g.chain([ + n(filter_dict={"timestamp": gt(pd.Timestamp(thirty_days_ago))}) +]) +``` + +### Business Hours Filtering + +```python +# Filter events during business hours +business_hours = g.chain([ + n(filter_dict={ + "timestamp": between(time(9, 0, 0), time(17, 0, 0)) + }) +]) +``` + +### Quarterly Data Analysis + +```python +# Q1 2023 data +q1_2023 = g.chain([ + n(filter_dict={ + "date": between( + date(2023, 1, 1), + date(2023, 3, 31) + ) + }) +]) +``` + +## Error Handling + +```python +# Strings raise ValueError - always use datetime objects +gt("2023-01-01") # ValueError: Raw string not allowed +gt(pd.Timestamp("2023-01-01")) # Correct: Use pandas Timestamp +``` + +## See Also + +- [GFQL Predicates API Reference](../api/gfql/predicates.rst) +- [GFQL Chain Operations](../api/gfql/chain.rst) +- [Wire Protocol Reference](wire_protocol_examples.md) \ No newline at end of file diff --git a/docs/source/gfql/index.rst b/docs/source/gfql/index.rst index bb6e3b485a..fd21b5f2ba 100644 --- a/docs/source/gfql/index.rst +++ b/docs/source/gfql/index.rst @@ -19,3 +19,5 @@ See also: combo quick predicates/quick + datetime_filtering + wire_protocol_examples diff --git a/docs/source/gfql/overview.rst b/docs/source/gfql/overview.rst index a08a2ae0b4..bbedaf9414 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -94,6 +94,30 @@ Example: Find nodes up to 2 hops away from node `"a"` and label each hop. first_hop_edges = g_2_hops._edges[ g_2_hops._edges["hop1"] == True ] print('Number of first-hop edges:', len(first_hop_edges)) +**Filter by Date/Time** + +Example: Find recent transactions using temporal predicates. + +.. code-block:: python + + from graphistry import n, e_forward + from graphistry.compute import gt, between + from datetime import datetime, date, time + import pandas as pd + + # Find transactions after a specific date + recent = g.chain([ + n(edge_match={"timestamp": gt(pd.Timestamp("2023-01-01"))}) + ]) + + # Find transactions in a date range during business hours + business_hours_txns = g.chain([ + n(edge_match={ + "date": between(date(2023, 6, 1), date(2023, 6, 30)), + "time": between(time(9, 0), time(17, 0)) + }) + ]) + **Query for Transaction Nodes Between Risky Nodes** Example: Find transaction nodes between two kinds of risky nodes. diff --git a/docs/source/gfql/predicates/quick.rst b/docs/source/gfql/predicates/quick.rst index 0fe7f7f09e..7eeda5a7b2 100644 --- a/docs/source/gfql/predicates/quick.rst +++ b/docs/source/gfql/predicates/quick.rst @@ -40,6 +40,15 @@ The following table lists the available operators, their descriptions, and examp - Between ``lower`` and ``upper`` (inclusive). - ``n({ "age": between(18, 65) })`` +.. note:: + All numeric comparison operators (``gt``, ``lt``, ``ge``, ``le``, ``eq``, ``ne``, ``between``) also support temporal values: + + - **DateTime**: ``n({ "created_at": gt(pd.Timestamp("2023-01-01 12:00:00")) })`` + - **Date**: ``n({ "event_date": eq(date(2023, 6, 15)) })`` + - **Time**: ``n({ "daily_time": between(time(9, 0), time(17, 0)) })`` + + See :doc:`/gfql/datetime_filtering` for datetime filtering examples. + **Categorical Operators** .. list-table:: diff --git a/docs/source/gfql/wire_protocol_examples.md b/docs/source/gfql/wire_protocol_examples.md new file mode 100644 index 0000000000..40f507b097 --- /dev/null +++ b/docs/source/gfql/wire_protocol_examples.md @@ -0,0 +1,516 @@ +# Temporal Predicates Wire Protocol Reference + +This document provides a comprehensive reference for how temporal predicates serialize to JSON in the GFQL wire protocol. The wire protocol enables interoperability between Python and other systems. + +## Overview + +The wire protocol uses tagged dictionaries to preserve type information during JSON serialization. This enables: +- Cross-language compatibility +- Configuration-driven predicate creation +- Network transport of queries +- Storage of predicate definitions + +**Key Concept**: Wire protocol dictionaries can be used directly in the Python API: + +```python +# These are equivalent: +pred1 = gt(100) +pred2 = gt(pd.Timestamp("2023-01-01")) +pred3 = gt({"type": "datetime", "value": "2023-01-01T00:00:00", "timezone": "UTC"}) +``` + +## 1. DateTime Comparisons + +### Python API +```python +import pandas as pd +from datetime import datetime +from graphistry import n +from graphistry.compute import gt, between + +# Using pandas Timestamp +filter1 = n(filter_dict={ + "created_at": gt(pd.Timestamp("2023-01-01 12:00:00")) +}) + +# Using Python datetime +filter2 = n(edge_match={ + "timestamp": between( + datetime(2023, 1, 1), + datetime(2023, 12, 31, 23, 59, 59) + ) +}) +``` + +### Wire Protocol (JSON) +```json +// GT with datetime +{ + "type": "ASTNode", + "filter_dict": { + "created_at": { + "type": "GT", + "val": { + "type": "datetime", + "value": "2023-01-01T12:00:00", + "timezone": "UTC" + } + } + } +} + +// Between with datetime range +{ + "type": "ASTNode", + "edge_match": { + "timestamp": { + "type": "Between", + "lower": { + "type": "datetime", + "value": "2023-01-01T00:00:00", + "timezone": "UTC" + }, + "upper": { + "type": "datetime", + "value": "2023-12-31T23:59:59", + "timezone": "UTC" + }, + "inclusive": true + } + } +} +``` + +### Round-trip Example +```python +# Create predicate +from graphistry.compute import gt +pred = gt(pd.Timestamp("2023-01-01 12:00:00")) + +# Serialize to JSON +json_data = pred.to_json() +print(json_data) +# Output: { +# 'type': 'GT', +# 'val': { +# 'type': 'datetime', +# 'value': '2023-01-01T12:00:00', +# 'timezone': 'UTC' +# } +# } + +# Deserialize from JSON +from graphistry.compute.predicates.numeric import GT +pred2 = GT.from_json(json_data) +# pred2 is functionally equivalent to pred +``` + +## 2. Date-Only Comparisons + +### Python API +```python +from datetime import date +from graphistry.compute import eq, ge + +# Date equality +filter1 = n(filter_dict={ + "event_date": eq(date(2023, 6, 15)) +}) + +# Date range check +filter2 = n(filter_dict={ + "start_date": ge(date(2023, 1, 1)) +}) +``` + +### Wire Protocol (JSON) +```json +// Date equality +{ + "type": "ASTNode", + "filter_dict": { + "event_date": { + "type": "EQ", + "val": { + "type": "date", + "value": "2023-06-15" + } + } + } +} + +// Date greater than or equal +{ + "type": "ASTNode", + "filter_dict": { + "start_date": { + "type": "GE", + "val": { + "type": "date", + "value": "2023-01-01" + } + } + } +} +``` + +## 3. Time-Only Comparisons + +### Python API +```python +from datetime import time +from graphistry.compute import is_in, between + +# Specific times +filter1 = n(filter_dict={ + "event_time": is_in([ + time(9, 0, 0), + time(12, 0, 0), + time(17, 0, 0) + ]) +}) + +# Time range +filter2 = n(edge_match={ + "daily_schedule": between( + time(9, 0, 0), + time(17, 30, 0) + ) +}) +``` + +### Wire Protocol (JSON) +```json +// IsIn with times +{ + "type": "ASTNode", + "filter_dict": { + "event_time": { + "type": "IsIn", + "options": [ + {"type": "time", "value": "09:00:00"}, + {"type": "time", "value": "12:00:00"}, + {"type": "time", "value": "17:00:00"} + ] + } + } +} + +// Time range +{ + "type": "ASTNode", + "edge_match": { + "daily_schedule": { + "type": "Between", + "lower": {"type": "time", "value": "09:00:00"}, + "upper": {"type": "time", "value": "17:30:00"}, + "inclusive": true + } + } +} +``` + +## 4. Timezone-Aware DateTime + +### Python API +```python +import pytz +from graphistry.compute import DateTimeValue, gt + +# Using timezone-aware timestamp +eastern = pytz.timezone('US/Eastern') +filter1 = n(filter_dict={ + "timestamp": gt( + pd.Timestamp("2023-01-01 09:00:00", tz=eastern) + ) +}) + +# Using DateTimeValue with explicit timezone +dt_val = DateTimeValue("2023-01-01T09:00:00", "US/Eastern") +filter2 = n(filter_dict={ + "timestamp": gt(dt_val) +}) +``` + +### Wire Protocol (JSON) +```json +// Timezone-aware datetime +{ + "type": "ASTNode", + "filter_dict": { + "timestamp": { + "type": "GT", + "val": { + "type": "datetime", + "value": "2023-01-01T09:00:00", + "timezone": "US/Eastern" + } + } + } +} +``` + +## 5. Complex Chain with Temporal Predicates + +### Python API +```python +from graphistry import n, e_forward +from graphistry.compute import gt, eq, between +from datetime import datetime, timedelta + +# Multi-hop query with temporal filters +chain = g.chain([ + # Recent transactions + n(edge_match={ + "timestamp": gt(datetime.now() - timedelta(days=7)), + "amount": gt(1000) + }), + # To active accounts + n(filter_dict={ + "status": eq("active"), + "last_login": between( + datetime.now() - timedelta(days=30), + datetime.now() + ) + }), + # Outgoing transfers + e_forward(edge_match={ + "type": eq("transfer"), + "timestamp": gt(datetime.now() - timedelta(days=1)) + }) +]) +``` + +### Wire Protocol (JSON) +```json +{ + "type": "Chain", + "queries": [ + { + "type": "ASTNode", + "edge_match": { + "timestamp": { + "type": "GT", + "val": { + "type": "datetime", + "value": "2023-12-18T10:30:00", + "timezone": "UTC" + } + }, + "amount": { + "type": "GT", + "val": 1000 + } + } + }, + { + "type": "ASTNode", + "filter_dict": { + "status": { + "type": "EQ", + "val": "active" + }, + "last_login": { + "type": "Between", + "lower": { + "type": "datetime", + "value": "2023-11-25T10:30:00", + "timezone": "UTC" + }, + "upper": { + "type": "datetime", + "value": "2023-12-25T10:30:00", + "timezone": "UTC" + }, + "inclusive": true + } + } + }, + { + "type": "ASTEdge", + "direction": "forward", + "edge_match": { + "type": { + "type": "EQ", + "val": "transfer" + }, + "timestamp": { + "type": "GT", + "val": { + "type": "datetime", + "value": "2023-12-24T10:30:00", + "timezone": "UTC" + } + } + } + } + ] +} +``` + +## 6. Temporal Value Classes Direct Usage + +### Python API +```python +from graphistry.compute import ( + DateTimeValue, DateValue, TimeValue, + temporal_value_from_json, gt +) + +# Create temporal values +dt_val = DateTimeValue("2023-06-15T14:30:00", "Europe/London") +date_val = DateValue("2023-06-15") +time_val = TimeValue("14:30:00") + +# Use in predicates +filter1 = n(filter_dict={"timestamp": gt(dt_val)}) +filter2 = n(filter_dict={"event_date": eq(date_val)}) +filter3 = n(filter_dict={"daily_time": eq(time_val)}) + +# Create from JSON +json_dt = { + "type": "datetime", + "value": "2023-06-15T14:30:00", + "timezone": "Europe/London" +} +dt_from_json = temporal_value_from_json(json_dt) +``` + +### Wire Protocol (JSON) +```json +// DateTimeValue serialization +{ + "type": "datetime", + "value": "2023-06-15T14:30:00", + "timezone": "Europe/London" +} + +// DateValue serialization +{ + "type": "date", + "value": "2023-06-15" +} + +// TimeValue serialization +{ + "type": "time", + "value": "14:30:00" +} +``` + +## 7. Full Round-Trip Example + +```python +# 1. Create a complex query with temporal predicates +from graphistry import n, Chain +from graphistry.compute import gt, between, is_in +from datetime import datetime, date, time +import pandas as pd + +query = Chain([ + n(filter_dict={ + "created": gt(pd.Timestamp("2023-01-01")), + "event_date": between(date(2023, 6, 1), date(2023, 6, 30)), + "event_time": is_in([time(9, 0), time(12, 0), time(17, 0)]) + }) +]) + +# 2. Serialize to JSON +json_query = query.to_json() +print(json_query) + +# 3. Send over wire (simulated) +import json +wire_data = json.dumps(json_query) +received_data = json.loads(wire_data) + +# 4. Deserialize on receiving end +from graphistry.compute.ast import Chain +reconstructed_query = Chain.from_json(received_data) + +# 5. Apply to graph data +result = g.chain(reconstructed_query.queries) +``` + +## Wire Protocol Structure + +### Temporal Value Types + +All temporal values in the wire protocol follow this pattern: + +```typescript +// DateTime with timezone +interface DateTimeWire { + type: "datetime"; + value: string; // ISO 8601 format + timezone?: string; // IANA timezone (default: "UTC") +} + +// Date only +interface DateWire { + type: "date"; + value: string; // YYYY-MM-DD format +} + +// Time only +interface TimeWire { + type: "time"; + value: string; // HH:MM:SS[.ffffff] format +} +``` + +### Predicate Structure + +Predicates containing temporal values serialize as: + +```typescript +interface TemporalPredicate { + type: "GT" | "LT" | "GE" | "LE" | "EQ" | "NE"; + val: DateTimeWire | DateWire | TimeWire; +} + +interface BetweenPredicate { + type: "Between"; + lower: DateTimeWire | DateWire | TimeWire; + upper: DateTimeWire | DateWire | TimeWire; + inclusive: boolean; +} + +interface IsInPredicate { + type: "IsIn"; + options: Array; +} +``` + +## Key Points + +1. **Type Safety**: Raw strings are rejected in the Python API to avoid ambiguity +2. **Automatic Conversion**: Python datetime objects are automatically converted to appropriate temporal values +3. **Timezone Preservation**: Timezone information is preserved through serialization +4. **Tagged Format**: JSON uses tagged dictionaries to preserve type information +5. **Direct Usage**: Wire protocol dicts can be passed directly to Python predicates + +## Error Handling + +```python +# Raw strings raise ValueError +try: + filter_raw = n(filter_dict={"date": gt("2023-01-01")}) +except ValueError as e: + print(e) + # Output: Raw string '2023-01-01' is ambiguous. Use: + # - gt(pd.Timestamp('2023-01-01')) for datetime + # - gt({'type': 'datetime', 'value': '2023-01-01T00:00:00'}) for explicit type + +# Valid approaches +filter1 = n(filter_dict={"date": gt(pd.Timestamp("2023-01-01"))}) +filter2 = n(filter_dict={"date": gt(datetime(2023, 1, 1))}) +filter3 = n(filter_dict={"date": gt({"type": "datetime", "value": "2023-01-01T00:00:00", "timezone": "UTC"})}) +``` + +## Performance Considerations + +- Temporal predicates leverage pandas' optimized datetime operations +- Timezone conversions are handled efficiently +- For large datasets, ensure datetime columns are properly typed (not object dtype) +- Use `pd.Timestamp` for best performance when creating many predicates programmatically \ No newline at end of file diff --git a/docs/source/notebooks/gfql.rst b/docs/source/notebooks/gfql.rst index dab38cf40e..d03accb479 100644 --- a/docs/source/notebooks/gfql.rst +++ b/docs/source/notebooks/gfql.rst @@ -7,6 +7,7 @@ GFQL Graph queries :titlesonly: Intro to graph queries with hop and chain <../demos/more_examples/graphistry_features/hop_and_chain_graph_pattern_mining.ipynb> + DateTime Filtering Examples <../demos/gfql/temporal_predicates.ipynb> GPU Benchmarking <../demos/gfql/benchmark_hops_cpu_gpu.ipynb> GFQL Remote mode <../demos/gfql/gfql_remote.ipynb> Python Remote mode <../demos/gfql/python_remote.ipynb>