Skip to content

Remote py apis - #2675

Draft
shivamka1 wants to merge 236 commits into
db_v4from
remote-py-apis
Draft

Remote py apis#2675
shivamka1 wants to merge 236 commits into
db_v4from
remote-py-apis

Conversation

@shivamka1

@shivamka1 shivamka1 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Remote Python APIs

Adds a Python RemoteGraph client that is a drop-in replacement for the local
Graph API, backed by a typed transport over GraphQL. Local code migrates by
swapping the constructor — method names, signatures, and semantics match the
local API; server-only extras are additive.

Architecture

  • Transport trait — the client core is execute(&Op) -> Result<Option<Prop>>
    over a typed operation tree (Op = Read(ReadExpr) | Write(WriteOp)). The
    GraphQL rendering lives behind the trait; nothing above it builds query strings.
  • Lazy views, one RPC per terminal — view chains (window, layers,
    at, filters, …) accumulate in a ReadExpr tree and fire a single request
    when a terminal (degree(), count(), collect(), …) is reached.
  • Static queries + JSON variables — all GraphQL is static text with
    serde-serialized variables (no string interpolation or templates). The serde
    impls on the wire types are the single source of truth for the format.
  • View-faithful collection materializationcollect() returns handles
    that replay the parent's op log (views + filters), so members evaluate under
    the same view chain as the collection they came from.
  • Time-semantics conversionevent_graph() / persistent_graph() on a
    remote graph reinterpret the same server graph under the other semantics,
    zero-copy: the server converts at load time via graph(path, graphType:).
    As locally, they apply to the base graph rather than to views.

Unified filtering

  • One filter(expr: GqlFilter) field on Graph/Node/Edge and all collections.
    GqlFilter is a oneOf input: nodes, edges, graph, and, or, not,
    plus flat graph-view spellings (window, at, layers, snapshotAt, …).
  • and is intersection, matching local g.filter(a & b); cross-kind or
    produces a proper graph union.
  • Python filter expressions (including graph-view chains and mixed node/edge
    compositions like filter.Graph.window(1, 4) & (filter.Node.property("x") > 1))
    are exported from core as a kind-tagged FilterTree and carried to the wire
    without loss.
  • Empty and/or lists are rejected — previously an empty or matched
    everything, which mattered for persisted access filters.

Wire fidelity

  • Exact dtypes — the server exposes a round-trippable PropertyType
    scalar and dtype fields; the client decodes stored values type-directed,
    so numeric widths (U8 stays U8), datetimes, and decimals survive the
    wire exactly. Older servers without dtype fall back to shape-based decoding.
  • Non-finite floats — NaN/±Infinity round-trip via tagged
    f64Special/f32Special input variants and string sentinels on output
    (previously: error on write, silent null on read).
  • Map orderingProp::Map is now insertion-ordered (IndexMap) and
    the wire preserves key order end to end, matching local behavior.

Performance

  • Response payloads are moved, not deep-copied, on every read RPC.
  • Edge.event(...) pins events via indexed layer/time views instead of
    scanning the full exploded history.
  • Client handles share state via Arc; nested columnar reads batch per
    source; history slicing fetches pages rather than full histories.

Breaking changes

GraphQL schema

  • filterNodes / filterEdges are removed. Migration is mechanical and
    can preserve response shapes with an alias:
    filterNodes(expr: X)filterNodes: filter(expr: { nodes: X }).
  • Node filters use per-field keys: {nodes: {name: {where: ...}}} /
    id: / nodeType:. The enum-argument spelling
    {node: {field: NODE_NAME, where: ...}} still works but is deprecated.
  • shrinkWindow is removed from all types and the apply-views inputs:
    internal_window already clamps requested bounds to the current view, so it
    was observably identical to window. shrinkStart / shrinkEnd remain
    (not expressible via window, which needs both bounds).
  • Persisted filters using the legacy node/edge keys still load (serde
    aliases); hand-written GraphQL must use nodes/edges.
  • PropertyOutput emits "NaN"/"Infinity"/"-Infinity" strings for
    non-finite floats where it previously emitted null.

Python API — removals (local and remote)

  • find_nodes / find_edges: legacy equality-only shortcuts fully subsumed
    by the filter API.
  • node[key] / edge[key] property reads: hid the property/metadata
    distinction, and node vs edge disagreed on missing keys (KeyError vs
    None). Use properties.get(key) / properties[key].
  • shrink_window: identical to window (see above). shrink_start /
    shrink_end remain.
  • Collection count(): a redundant alias; use len(collection). (Removed
    from local Edges and every remote collection.)
  • RemoteTemporalProperty.latest(): a redundant alias of value(), which
    local never had.

Python API — behaviour changes

  • Node ids are typed end to end: integer ids stay integers over the wire,
    and a write with the wrong id type is refused exactly as the local Graph
    refuses it. (Previously ints were silently stringified.)
  • Tuple times are rejected on writes: add_node((5, 3), ...) raises the
    same TypeError as local instead of silently dropping the event id.
  • Collection keys() returns the graph's registered keys for the entity
    kind (matching local), instead of the first member's keys — previously
    values() / items() / as_dict() could silently drop whole columns.
  • property_type returns a PropType object instead of a display
    string, so schema comparisons are order-stable.
  • RemotePathFromGraph.count() removed (nested count was ambiguous; use
    len()).

Python API — additions (local, closing remote↔local gaps)

  • filter(expr) on Edge, Edges, and NestedEdges (core supported it;
    the bindings were missing).
  • sorted(sort_bys) on Nodes and Edges, with NodeSortBy /
    EdgeSortBy / SortByTimeone shared set of classes used by both
    local and remote, so drop-in code passes the same key objects to either
    side. The sort comparators moved from the GraphQL crate into core; the
    server now delegates.

Build fix

pyo3/extension-module is now pinned under [tool.maturin] — without it the
wheel links libpython and import raphtory segfaults on statically-linked
interpreters (e.g. conda).

Testing

  • Local-vs-remote parity e2e suite covering the full client surface
    (views, filters, collections, properties, write path), including key-order,
    NaN, and dtype round-trips.
  • GraphQL filter suites migrated to the unified field.
  • Rust unit tests for wire round-trips (filters, values, special floats).

Not in this PR (local APIs the remote client does not yet cover)

  • Rolling / expanding windowsrolling() and expanding() (and the
    WindowSet they return) are not available on any remote object (Remote expanding() and rolling() window sets #2723).
    All other time views (window, at, before, after, snapshot_*,
    shrink_start, shrink_end) are supported.

  • Collection-level history and node states.history on collections
    (and .deletions on edge collections) are not exposed remotely; per-entity
    node.history / edge.deletions are. Same root gap: there is no remote
    NodeState, so collection accessors like nodes.degree() return plain lists
    rather than the sortable/groupable NodeState views the local API returns
    (Remote NodeState subsystem: collection history/deletions, columnar temporal view, history merge #2722, which depends on the server-side Implement the GraphQL NodeState type properly #2730).

  • Algorithms — the raphtory.algorithms module (and
    Graph.largest_connected_component) operate on local graphs only.

  • Vectors / embeddingsGraph.vectorise and the raphtory.vectors
    module (server-side vector endpoints remain reachable via raw
    client.query(...) on vectors-enabled servers).

  • Materializationmaterialize, materialize_at: building a new
    standalone graph out of a (possibly viewed) graph needs server-side graph
    creation, so it is deferred. Flavour conversion is not part of this gap
    and is supported — see above.

  • Minor surface gaps — columnar collection property views lack the
    collection-level .temporal view; History.merge / compose_histories
    are unavailable on remote history handles. (Both ledgered as strict xfails
    in the parity suite, tracked in Remote NodeState subsystem: collection history/deletions, columnar temporal view, history merge #2722.)

  • DataFrame / Parquet bulk IOload_nodes / load_edges /
    load_*_metadata, from_parquet / to_parquet. Batch remote writes exist
    (RemoteGraph.add_nodes / add_edges) but take update lists, not
    DataFrames — a deliberate remote-only extra that amortizes network
    round-trips, with no local counterpart planned.

  • Exportto_networkx, to_pyvis, and collection to_df.

  • Cross-graph import — the import_node(s) / import_edge(s) (_as)
    family, which copies entities from another in-process graph.

  • Local persistence (not applicable remotely)save_to_file /
    load_from_file / serialise / deserialise and the disk cache
    (cache_view, flush) are in-process concepts; whole-graph transfer is
    covered by RaphtoryClient.send_graph / receive_graph / upload_graph.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Rust Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: 2c7186b Previous: 9823ef7 Ratio
lotr_graph/num_edges 4 ns/iter (± 0) 0 ns/iter (± 0) +∞
lotr_graph/num_nodes 4 ns/iter (± 0) 1 ns/iter (± 0) 4
lotr_graph/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph/graph_latest 3 ns/iter (± 0) 0 ns/iter (± 0) +∞
lotr_graph_materialise/materialize 8193499 ns/iter (± 83355) 1564816 ns/iter (± 35303) 5.24
lotr_graph_window_100/num_nodes 14 ns/iter (± 0) 5 ns/iter (± 0) 2.80
lotr_graph_window_100_materialise/materialize 8473612 ns/iter (± 84159) 1669150 ns/iter (± 10700) 5.08
lotr_graph_window_10/has_node_existing 146 ns/iter (± 10) 62 ns/iter (± 11) 2.35
lotr_graph_window_10_materialise/materialize 3430068 ns/iter (± 10169) 971980 ns/iter (± 4278) 3.53
lotr_graph_subgraph_10pc/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph_subgraph_10pc_materialise/materialize 2020046 ns/iter (± 28225) 334634 ns/iter (± 1287) 6.04
lotr_graph_subgraph_10pc_windowed/has_node_existing 152 ns/iter (± 10) 62 ns/iter (± 14) 2.45
lotr_graph_subgraph_10pc_windowed_materialise/materialize 1217668 ns/iter (± 19805) 230399 ns/iter (± 2617) 5.29
lotr_graph_window_50_layered/num_edges_temporal 151553 ns/iter (± 1304) 70121 ns/iter (± 7586) 2.16
lotr_graph_window_50_layered/has_node_existing 387 ns/iter (± 25) 129 ns/iter (± 12) 3
lotr_graph_window_50_layered/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph_window_50_layered/max_neighbour_degree 847960 ns/iter (± 19297) 386651 ns/iter (± 3853) 2.19
lotr_graph_window_50_layered/graph_latest 83486 ns/iter (± 1318) 36649 ns/iter (± 916) 2.28
lotr_graph_window_50_layered_materialise/materialize 31815666 ns/iter (± 235486) 3488825 ns/iter (± 24948) 9.12
lotr_graph_persistent_window_50_layered/num_edges_temporal 648085 ns/iter (± 9447) 192686 ns/iter (± 1569) 3.36
lotr_graph_persistent_window_50_layered/has_node_existing 437 ns/iter (± 434) 174 ns/iter (± 83) 2.51
lotr_graph_persistent_window_50_layered/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph_persistent_window_50_layered/iterate_exploded_edges 3642227 ns/iter (± 35164) 1659940 ns/iter (± 19402) 2.19
lotr_graph_persistent_window_50_layered/graph_latest 125161 ns/iter (± 1197) 57549 ns/iter (± 4809) 2.17
lotr_graph_persistent_window_50_layered_materialise/materialize 54898390 ns/iter (± 223760) 5298035 ns/iter (± 147912) 10.36

This comment was automatically generated by workflow using github-action-benchmark.

github-actions Bot and others added 20 commits July 3, 2026 22:37
…Degree/OutDegree/Name) with Python bindings + tests
…ypes, ExcludeNodes) + Valid, DefaultLayer, and graph Path/Namespace/Name terminals
…verse views

to_list/to_list_rev duplicated collect/collect_rev on the history
sub-collections, locally and remotely; both sets are gone. __reversed__ on
the remote history and its sub-collections now goes through a lazy
reverse() view backed by the server's reverse field, so it stays one RPC
and composes with any future optimised iterator instead of materializing a
reversed list. Interval reversal keeps the local semantics: reversing the
history flips the sign of the deltas.
len() and `in` on the remote history and its sub-collections downloaded
the entire history to answer a scalar question. The server's History type
gains contains (by timestamp, optionally with the event id), and the
timestamps, event-id and intervals sub-views gain count and contains; the
client asks those instead. Membership takes whatever converts to an
EventTime, exactly as locally — a bare int means (t, event_id=0), and a
naive datetime is not a member and never reaches the wire.
The columnar views fetched per-member {key, value} entries and pivoted
them by matching the key — after the wire had already been asked for a
specific column. Requested columns now render as aliased single-key gets
and decode straight into columns, so get() takes its column rather than
searching for it, and ColumnarProps, member_value and fetch_with_keys are
gone. The all-columns fetch was dead code, so keys is no longer optional:
one wire shape, one decode shape. A missing alias in the response is a
protocol error, distinct from null, which is an absent value.
Renames GqlNestedEdges.nn to edges and the nested-collection count to len
(it is the outer source count, as local NestedEdges.len is), deletes the
empty matching module, drops 29 redundant nested json! calls, returns the
bound dict and iterator directly instead of erasing and unbinding them,
and corrects the reverse-filter-conversion comment: the client sends the
unified GqlFilter, and a Layer::All view op is dropped rather than
rejected.

parse_read now takes the response map itself: the first path segment
indexes it directly, so the response is no longer re-wrapped into a
JsonValue per read, and everything below the root field borrows.
prop_str, prop_list and prop_map_get join the expect_* family in the
transport module, where every other result decoder lives; remote_schema
keeps only its own record shapes. The graph model modules and their
resolver types are public so plugins can extend them.
PathFromGraph.count is the number of sources, not of neighbours; local
spells that len(), so the client does too and the python-only count()
wrapper goes. The single-handle server fixture moves to test_utils beside
the one it wraps, and a few test nits: the confusing filter docstring says
what it means, a redundant hasattr assertion and some list() wrappers are
gone.
# Conflicts:
#	python/python/raphtory/__init__.pyi
#	python/python/raphtory/graphql/__init__.pyi
#	raphtory-graphql/src/model/graph/edges.rs
#	raphtory-graphql/src/model/graph/filtering.rs
#	raphtory-graphql/src/model/graph/nodes.rs
#	raphtory-graphql/src/model/graph/path_from_node.rs
#	raphtory/src/db/graph/edges.rs
#	raphtory/src/db/graph/nodes.rs
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.

3 participants