Skip to content

feat(gfql): Add metadata hydration for remote GFQL responses - #798

Merged
lmeyerov merged 20 commits into
masterfrom
feat/gfql-remote-metadata-hydration
Oct 17, 2025
Merged

lmeyerov merged 20 commits into
masterfrom
feat/gfql-remote-metadata-hydration

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Oct 17, 2025 •

Copy link
Copy Markdown
Contributor

Summary

Implements TDD-based solution to hydrate server-computed metadata back into Plottable objects after gfql_remote() operations. When GFQL operations like call('umap') modify bindings/encodings on the server, those changes are now transferred back to the client.

Problem

Previously, gfql_remote() returned DataFrames but lost all metadata that the server computed during GFQL operations. Example scenario:

g1 = graphistry.nodes(df, 'id')
g2 = g1.gfql_remote(call('umap', {'X': ['x', 'y']}))
# Server ran UMAP, changed bindings, added color encodings
# But g2 had none of this metadata!

Solution

Created new graphistry/io/metadata.py module for unified metadata serialization/deserialization:

  • Deserializer: deserialize_plottable_metadata() - hydrates metadata from server responses
  • Serializer: serialize_plottable_metadata() - extracts metadata for upload (foundation for future work)
  • Integrated into chain_remote.py for both JSON and zip response formats
  • TypedDict structures: Properly typed metadata with ComplexEncodingsDict throughout codebase

Key Features

✅ TDD approach: All 12 tests written first, failed initially, now passing (100%)
✅ Backwards compatible: Works with old servers that don't return metadata field
✅ Graceful degradation: Missing/malformed metadata logs warnings, doesn't break
✅ New io/ module: Single source of truth for metadata format consistency
✅ Type safety: ComplexEncodingsDict with literal keys (zero type: ignore comments)
✅ Comprehensive coverage: Bindings, encodings, metadata, style, edge cases

Architecture

  • graphistry/io/types.py (146 lines): TypedDict structures for metadata
  • graphistry/io/metadata.py (370 lines): Serialization/deserialization logic
  • chain_remote.py: Hydration for both JSON (metadata) and zip (gfql_metadata) formats
  • Refactored arrow_uploader: Removed 76 lines of duplicated code, uses centralized metadata functions
  • Proper typing: _complex_encodings typed as ComplexEncodingsDict in Plottable/PlotterBase

Test Coverage

New tests (12 total, 436 lines):

  • test_umap_bindings_hydrated - Bindings transfer from server
  • test_umap_encodings_hydrated - Simple + complex encodings transfer
  • test_name_description_hydrated - Metadata fields transfer
  • test_style_hydrated - Style configuration transfers
  • test_empty_metadata_doesnt_break - Backwards compatibility with old servers
  • test_partial_metadata_hydrated - Partial metadata works
  • test_full_metadata_hydrated - Complete metadata scenario
  • test_metadata_preserves_existing_dataframes - DataFrames not modified
  • test_zip_format_metadata_hydrated - Zip format support
  • test_malformed_metadata_graceful_handling - Error resilience
  • test_metadata_with_none_values - None value handling
  • test_metadata_overrides_existing_bindings - Server precedence

Existing tests:

  • ✅ All persistence tests still passing (zero regressions)
  • ✅ Mypy clean (zero type errors)

Files Changed

Created:

  • graphistry/io/__init__.py - Module exports
  • graphistry/io/types.py - TypedDict structures (146 lines)
  • graphistry/io/metadata.py - Serialization/deserialization (370 lines)
  • graphistry/tests/test_gfql_remote_metadata.py - Test suite (495 lines, 12 tests)

Modified:

  • graphistry/Plottable.py - Typed _complex_encodings
  • graphistry/PlotterBase.py - Refactored to use ComplexEncodingsDict, literal keys
  • graphistry/arrow_uploader.py - Refactored to use io module (-76 lines)
  • graphistry/compute/chain_remote.py - Added metadata hydration, hoisted imports
  • graphistry/io/metadata.py - Hoisted imports

Backwards Compatibility

Old servers (no metadata field):

  • ✅ Request succeeds normally
  • ✅ Returns DataFrames as before
  • ✅ Hydration skipped silently
  • ✅ No errors, no warnings

New servers (with metadata field):

  • ✅ Request succeeds
  • ✅ Returns DataFrames + metadata
  • ✅ Hydration applied automatically
  • ✅ Server-computed metadata transferred

Testing

# Run new tests
pytest graphistry/tests/test_gfql_remote_metadata.py -v
# 12 passed

# Type checking
mypy graphistry/io/metadata.py graphistry/io/types.py
# Success: no issues found

🤖 Generated with Claude Code

lmeyerov and others added 4 commits October 17, 2025 02:40
Initial TDD plan for hydrating server metadata from gfql_remote()
responses back into client Plottable. Covers bindings, encodings,
metadata, and style.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implement TDD-based solution to hydrate server-computed metadata back into
Plottable objects after gfql_remote() operations. When GFQL operations like
call('umap') modify bindings/encodings on the server, those changes are now
transferred back to the client.

**New Features:**
- Created graphistry/io/metadata.py module for unified metadata serialization/deserialization
- Implemented deserialize_plottable_metadata() to hydrate bindings, encodings, metadata, and style
- Integrated hydration into chain_remote.py for both JSON and zip response formats
- Added 12 comprehensive tests with 100% pass rate

**Architecture:**
- New graphistry/io/ module separates serialization concerns from uploader/plotter
- Thin wrapper in PlotterBase._hydrate_metadata_from_response() delegates to io module
- Graceful error handling with warnings for malformed metadata
- Backward compatible - zero regressions in existing tests (13/13 passing)

**Test Coverage:**
- test_gfql_remote_metadata.py: 12 tests covering bindings, encodings, metadata, style
- Edge cases: empty metadata, partial metadata, malformed data, None values
- Zip and JSON format support validated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Use TYPE_CHECKING pattern to import Plottable for type hints
- Avoids circular import issues while maintaining type safety
- Flake8 clean with Python 3.12
… Plottable method

- Remove _hydrate_metadata_from_response() method from PlotterBase
- Import deserialize_plottable_metadata() directly in chain_remote.py
- Cleaner separation of concerns - I/O logic stays in io module
- Eliminates mypy errors about missing Plottable attributes
- All 12 metadata hydration tests passing
Comment thread ai/prompts/PLAN.md
@@ -1,234 +1,288 @@
# Task Plan Template

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

Comment thread graphistry/compute/chain_remote.py
lmeyerov and others added 2 commits October 17, 2025 03:12
…plan)

- Restored ai/prompts/PLAN.md to its original template state
- Feature plan moved to plans/gfql-remote-metadata-hydration/plan.md (gitignored)
- ai/prompts/ is for general AI assistant guidance templates
- plans/ is for specific feature/bug fix plans
…ialization logic

DRY violation fix:
- arrow_uploader.py now imports and delegates to io.metadata functions
- Removed ~70 lines of duplicate code (maybe_bindings, g_to_*_bindings, g_to_*_encodings)
- Single source of truth for metadata serialization in io/metadata.py
- All tests passing (12 metadata tests + 18 arrow uploader tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@lmeyerov lmeyerov linked an issue Oct 17, 2025 that may be closed by this pull request
lmeyerov and others added 8 commits October 17, 2025 03:42
- Add full type signatures to all functions with specific types
- Use Dict[str, str] for bindings (not Dict[str, Any])
- Use List[str] for field mappings (not bare List)
- Add type annotations to all local variables
- Avoid bare Any type where specific types are known
- Type hints: bindings, encodings, metadata_obj, style, result

All functions now have complete parameter and return type annotations
following Python typing best practices.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add precise type definitions using TypedDict to lock in the exact
structure of metadata JSON serialization:

- PlottableMetadata: Top-level structure (bindings, encodings, metadata, style)
- EncodingsDict: All encoding keys (point_color, edge_color, complex_encodings, etc.)
- MetadataDict: Graph metadata (name, description)

Benefits:
- Static type checking for exact keys and value types
- IDE autocomplete for metadata structure fields
- Compile-time validation of metadata structure
- Clear documentation of expected JSON format
- No runtime behavior changes (TypedDict is structural)

All fields use total=False to make them optional (only present fields
are included in serialization). Type ignore comments added where dynamic
key access is required for flexibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Move all metadata TypedDict definitions from metadata.py to new types.py
module for better organization and separation of concerns.

New structure:
- graphistry/io/types.py: All TypedDict definitions
  - PlottableMetadata (top-level)
  - EncodingsDict (simple encodings)
  - MetadataDict (name, description)
  - ComplexEncodingsDict (complex encodings structure)
  - ComplexEncodingModes (default/current modes)
  - ComplexEncodingMode (individual encoding definitions)

- graphistry/io/metadata.py: Serialization/deserialization functions
  - Imports types from graphistry.io.types

Benefits:
- Better code organization (types separated from logic)
- Easier to find and maintain type definitions
- Can be imported independently for type hints
- Follows common pattern (types/ or models/ folder)
- Added comprehensive TypedDict for complex_encodings structure

All tests passing (12/12), linting clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add comprehensive protocol for removing redundant comments from PRs while
preserving valuable documentation. Follows 4-phase TDD-like approach:

Phases:
1. Identify: Generate inventory of all comments added in PR with context
2. Categorize: Classify each comment as KEEP or REMOVE based on value criteria
3. Remove: Systematically delete redundant comments, commit frequently
4. Verify: Extra pass reviewing all removals, ensure no valuable comments lost

KEEP criteria (preserve these):
- Non-obvious behavior explanations
- GitHub issue references (#123, etc.)
- TODOs and action items
- Bug workarounds
- Performance/security notes
- Type ignore explanations
- Complex algorithm explanations

REMOVE criteria (redundant with code):
- Obvious from code ("Set x to 5" before x = 5)
- Redundant with variable/function names
- Ephemeral dev notes (WIP, debug, testing)
- Redundant with docstrings
- Unnecessary section markers
- Commented-out code without explanation

Features:
- Plan integration (creates phase in existing plan or new plan)
- Examples for each category (keep vs remove)
- Git workflow integration (conventional commits)
- Success criteria per phase
- Common pitfalls to avoid
- Comprehensive checklist

Usage: Run after PR implementation complete, before requesting review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove 10 redundant comments that were obvious from code context:
- arrow_uploader.py: "Delegate to io.metadata" (obvious from delegation)
- chain_remote.py: "Handle persist response" and similar (obvious from conditionals)
- metadata.py: Redundant section comments and obvious descriptions

All comments removed were categorized as REMOVE per DECOMMENT protocol:
- Obvious from code
- Redundant with variable/function names
- Redundant with immediate context

Kept 28 valuable comments:
- Section markers in large functions
- Non-obvious behavior explanations
- Backwards compatibility notes
- Type ignore overrides

Tests: 12/12 passing ✅
Linting: 0 errors ✅

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…otocol

Add executable script to automate Phase 1 of DECOMMENT protocol:
- Extracts all added comments from PR diff
- Formats with context for categorization
- Generates markdown inventory template
- Reduces Phase 1 time from 5 minutes to 30 seconds (10x speedup)

Based on execution analysis showing Phase 1 manual inventory was slowest
part of protocol (25% of total time).

Usage: ./ai/assets/generate_comment_inventory.sh [base_branch] [output_file]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Rewrite using pure bash for better portability and simpler logic.
Captures Python-style comments (#) from PR diffs.

Note: Currently captures all # patterns including shebangs - future
improvement could filter these out, but script is functional for manual
review workflow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Comment thread graphistry/compute/chain_remote.py Outdated
# Check for metadata.json in zip (both persist and GFQL metadata)
if 'metadata.json' in zip_ref.namelist():
try:
import json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hoist import

Comment thread graphistry/io/metadata.py
lmeyerov and others added 6 commits October 17, 2025 15:32
- Create ai/prompts/HOISTIMPORTS.md protocol for hoisting dynamic imports
- Add ai/assets/find_dynamic_imports.sh automation script
- Follow DECOMMENT pattern for consistency
- Include import ordering rules (stdlib, third-party, internal absolute, internal relative)
- Emphasize "ninja mode" - insert without resorting existing imports
- Automate Phase 1 inventory generation (10x speedup)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add NodeEdgeEncodingsDict for serialize_node/edge_encodings return types
- Update serialize_node_encodings and serialize_edge_encodings to return NodeEdgeEncodingsDict
- Add type annotations for complex_encodings using ComplexEncodingsDict
- Add type: ignore comments where Plottable._complex_encodings is Dict[Any, Any]
- All mypy checks passing
- All tests passing (12/12)

This completes the static semantics work - all functions now use the
TypedDict types we created instead of generic Dict[str, Any].

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…gsDict

- Change ComplexEncodingModes and ComplexEncodingsDict from total=False to total=True
- This makes 'current', 'default', 'node_encodings', 'edge_encodings' required keys
- Refactor code to avoid dynamic key access (no f-strings for dict keys)
- Replace loops with explicit 'current' and 'default' access
- Replace f'{graph_type_2}_encodings' with explicit if/else branches
- Update Plottable.py and PlotterBase.py to use ComplexEncodingsDict type
- Zero type: ignore comments needed - fully typed!
- All mypy checks passing
- All tests passing (12/12)

This completes proper TypedDict typing throughout the codebase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fix bash regex pattern for detecting dynamic imports in git diff output.
Changed `^\\+` to `^\+` to properly match added lines with indentation.

The double backslash was causing the pattern to look for a literal
backslash character instead of matching the diff '+' prefix, resulting
in 0 imports found when there were actually 8 dynamic imports present.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Move all dynamic imports from function/method scope to module-level
imports following PEP 8 import ordering conventions.

Changes:
- graphistry/compute/chain_remote.py: Hoist json, uuid, warnings, DatasetInfo
- graphistry/io/metadata.py: Hoist warnings
- graphistry/tests/test_gfql_remote_metadata.py: Hoist zipfile, json, BytesIO

All hoisted imports are stdlib or internal modules with no circular
dependency issues. Reduces code complexity and improves import
organization. Net -8 lines.

Tests: 12 passed in test_gfql_remote_metadata.py
Type checking: mypy clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add entry for PR #798 documenting:
- GFQL metadata hydration fix
- Metadata serialization centralization
- TypedDict typing improvements
- Import organization refactoring

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@lmeyerov
lmeyerov merged commit 0e0023a into master Oct 17, 2025
28 of 29 checks passed
@lmeyerov
lmeyerov deleted the feat/gfql-remote-metadata-hydration branch October 17, 2025 23:38
@lmeyerov lmeyerov mentioned this pull request Oct 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UMAP upload sends incorrect edge bindings after creating hypergraph

1 participant