Skip to content

Remote py apis - #2675

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

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

Conversation

@shivamka1

@shivamka1 shivamka1 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Transport abstraction for the RemoteGraph client

Motivation

The existing Python RemoteGraph client is write-only, with each of ~15
mutation methods inlining its own Jinja template and JSON-parsing chain.
This PR consolidates that plumbing into a single Transport abstraction
that every operation flows through, and adds a minimal composable read
surface on top of the same seam so the client can now do lazy view
composition like rg.window(0, 10).node("ben").degree() end-to-end.

What shipped

  • New Transport trait in
    raphtory-graphql/src/client/transport.rs
    async fn execute(&self, op: &Op) -> Result<Option<Prop>, ClientError>.
  • Op { Read(ReadExpr) | Write(WriteOp) } in
    op.rs — read side is a
    recursive expression tree, write side is a flat enum of arg structs.
  • GraphqlTransport in
    graphql_transport.rs
    renders write ops via the existing Jinja templates (moved from client
    wrappers), walks read expressions into nested GraphQL queries, parses
    responses.
  • All 15 individual writes migrated through the transport, byte-identical
    wire behavior: 7 on RemoteGraph, 4 on RemoteNode, 4 on RemoteEdge.
    Each method body shrinks from ~30–50 lines to ~5–15 lines.
  • Minimal read surface: Root, Window, Node, Degree — enough for
    rg.window(s, e).node(id).degree() end-to-end. Only the terminal fires
    an RPC.
  • Renames: GraphQLRemote{Graph,Node,Edge}Remote*,
    RaphtoryGraphQLClientRemoteClient, raphtory_client.rs
    remote_client.rs. Python-facing names unchanged.
  • Cleanup: client field removed from RemoteNode/RemoteEdge — they
    now hold only transport and view state. Kept on RemoteGraph pending
    batch-method migration (see below).
  • Bug fix: PyRemoteGraph::node/edge now delegate to the Rust methods,
    so the accumulated view chain (window(...)) is correctly propagated
    when descending to a node — previously silently dropped.
  • Python bindings added for window() and degree().

Testing

  • 46 pre-existing raphtory-graphql tests still green.
  • 3 new Rust tests in graphql_transport.rs (unit tests for the read
    render/parse; one integration test spawning a real server).
  • 3 new Python tests in
    test_remote_graph_transport.py:
    test_add_and_degree, test_windowed_degree, test_view_chain_propagation.

TODO

  • Two Python-side batch mutations (add_nodes, add_edges) still call
    client.query(...) directly (existing TODOs). Migrating them removes
    the last client field.
  • Read surface expansion (layer, at, rolling, nodes, ids, name, count,
    properties, ...).
  • One hard-shape algorithm end-to-end (e.g. WCC) to lock in the
    return-type conversion pattern.
  • Iteration + paging for for n in g.nodes: ....
  • Small LLM eval to retire the product bet.

@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: 2b624f9 Previous: 9823ef7 Ratio
lotr_graph/num_edges 4 ns/iter (± 0) 0 ns/iter (± 0) +∞
lotr_graph/num_nodes 5 ns/iter (± 0) 1 ns/iter (± 0) 5
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 8050631 ns/iter (± 40172) 1564816 ns/iter (± 35303) 5.14
lotr_graph_window_100/num_nodes 15 ns/iter (± 0) 5 ns/iter (± 0) 3
lotr_graph_window_100_materialise/materialize 8089774 ns/iter (± 64519) 1669150 ns/iter (± 10700) 4.85
lotr_graph_window_10/has_node_existing 143 ns/iter (± 9) 62 ns/iter (± 11) 2.31
lotr_graph_window_10_materialise/materialize 3345959 ns/iter (± 47014) 971980 ns/iter (± 4278) 3.44
lotr_graph_subgraph_10pc/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph_subgraph_10pc_materialise/materialize 2009904 ns/iter (± 22468) 334634 ns/iter (± 1287) 6.01
lotr_graph_subgraph_10pc_windowed/has_node_existing 149 ns/iter (± 9) 62 ns/iter (± 14) 2.40
lotr_graph_subgraph_10pc_windowed_materialise/materialize 1217703 ns/iter (± 11569) 230399 ns/iter (± 2617) 5.29
lotr_graph_window_50_layered/num_edges_temporal 155717 ns/iter (± 2232) 70121 ns/iter (± 7586) 2.22
lotr_graph_window_50_layered/has_node_existing 390 ns/iter (± 24) 129 ns/iter (± 12) 3.02
lotr_graph_window_50_layered/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph_window_50_layered/graph_latest 85415 ns/iter (± 2064) 36649 ns/iter (± 916) 2.33
lotr_graph_window_50_layered_materialise/materialize 30515575 ns/iter (± 95819) 3488825 ns/iter (± 24948) 8.75
lotr_graph_persistent_window_50_layered/num_edges_temporal 646047 ns/iter (± 24446) 192686 ns/iter (± 1569) 3.35
lotr_graph_persistent_window_50_layered/has_node_existing 442 ns/iter (± 435) 174 ns/iter (± 83) 2.54
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 3480647 ns/iter (± 10244) 1659940 ns/iter (± 19402) 2.10
lotr_graph_persistent_window_50_layered/graph_latest 137704 ns/iter (± 4154) 57549 ns/iter (± 4809) 2.39
lotr_graph_persistent_window_50_layered_materialise/materialize 53184087 ns/iter (± 282815) 5298035 ns/iter (± 147912) 10.04

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

@ljeub-pometry ljeub-pometry left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Correctness:

  • Make sure property types match between remote/local apis (need to pass along the data type to cast the output correctly)
  • Filter apis in graphql need to be aligned with rust/python such that all filters pass through the remote client correctly
  • get_dtype_of needs to return PropType, not String
  • Change the inner type of Prop::Map to IndexMap, so we don't have to worry about scrambling the order everywhere

Performance:

  • A lot of String allocations when building the queries can be avoided by passing in a mutable String as a buffer
  • event and event_layer on edges need to be implemented efficiently as part of EdgeViewOps instead of doing a linear search over the exploded edges

Tidy:

  • the client modules can be tidied up a bit
  • a lot of unnecessary manual conversion to Py<PyAny>> in the python apis

local = Graph()
build(local)

with GraphServer(tempfile.mkdtemp()).start() as server:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use with tempfile.TemporaryDirectory() as work_dir: to make sure your tests actually clean up after themselves


A context manager — the server is started on enter and torn down on exit.
"""
work_dir = tempfile.mkdtemp()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use with tempfile.TemporaryDirectory() as work_dir: to make sure your tests actually clean up after themselves

Comment on lines +35 to +37
rg.add_node(1, "ben")
rg.add_node(2, "hamza")
rg.add_edge(3, "ben", "hamza")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if you take these out of the context manager setup code and put them in a separate function, you can reuse your server startup context manager for all the tests

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

might need to pass the graph name and type as input arguments for the context manager

work_dir = tempfile.mkdtemp()
with GraphServer(work_dir).start() as server:
client = server.get_client()
client.new_graph("g", "EVENT")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this should return a remote graph (it might already?)

}
"#;

let variables = HashMap::from([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

not your fault, but this seems to cause a lot of intermediate allocations due to the input type for the variables

// The two view types share an identical method surface and pivot logic; the
// only difference is which client handle (and thus which container) they wrap.
// A macro keeps the two `#[pymethods]` blocks in lockstep without duplication.
macro_rules! columnar_view_methods {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this seems to be missing get_dtypes_of

///
/// Returns:
/// Optional[str]: the property's data-type, or None if absent.
pub fn get_dtype_of(&self, key: String) -> Result<Option<String>, ClientError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this needs to return the property type, not a string

Comment on lines +24 to +32
/// Convert a `Prop` value into a native Python object — the raw value a local
/// `Properties`/`Metadata` `.get()`/`.values()` returns (drop-in parity; no
/// `RemoteProperty` wrapper). Used by the non-temporal containers.
fn prop_to_py(py: Python<'_>, value: Prop) -> Result<Py<PyAny>, ClientError> {
Ok(value
.into_pyobject(py)
.map_err(|e| ClientError::InvalidResponse(e.to_string()))?
.unbind())
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this seems completely pointless, can just return the Prop

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

although, maybe this should take a dtype and handle casting...

}

#[pymethods]
impl PyRemoteMetadata {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

all the Py<PyAny>> can just be Prop, let pyo3 generate the conversion code

Comment on lines +63 to +70
pub fn filter(&self, filter: PyFilterExpr) -> PyResult<PyRemoteNestedEdges> {
let composite = filter
.try_as_edge_filter()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let gql_filter = composite
.try_into()
.map_err(|e: raphtory::errors::GraphError| PyValueError::new_err(e.to_string()))?;
Ok(PyRemoteNestedEdges::new(self.edges.filter(gql_filter)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

need to support all valid filters

shivamka1 and others added 15 commits August 4, 2026 11:11
…add items() for pairs; drop the RemoteProperty wrapper
…p: ViewOp }: one vocabulary, data-driven render/parse, ctx inspectable
Co-authored-by: Shivam <4599890+shivamka1@users.noreply.github.com>
… everything — a fail-open for stored access filters
…accept string GIDs in id ordering, reject degree op-chains early, normalize Layer::None/All
…rsion; legacy node/edge keys still load via serde aliases
…e/Edge/collections; remove filterNodes/filterEdges; collapse the client's six filter ops into one
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