rxgraph 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {rxgraph-0.6.0 → rxgraph-0.7.0}/Cargo.lock +5 -16
  2. {rxgraph-0.6.0 → rxgraph-0.7.0}/Cargo.toml +1 -1
  3. {rxgraph-0.6.0 → rxgraph-0.7.0}/PKG-INFO +12 -2
  4. {rxgraph-0.6.0 → rxgraph-0.7.0}/README.md +11 -1
  5. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/Cargo.toml +12 -1
  6. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/README.md +5 -5
  7. rxgraph-0.7.0/crates/rxgraph/examples/native_row_lazy.rs +378 -0
  8. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/lib.rs +6 -1
  9. rxgraph-0.6.0/crates/rxgraph-py/src/lib.rs → rxgraph-0.7.0/crates/rxgraph/src/python.rs +9 -9
  10. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/mod.rs +2 -0
  11. rxgraph-0.7.0/crates/rxgraph/src/traversal/native.rs +1126 -0
  12. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph-python/Cargo.toml +2 -2
  13. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph-python/src/lib.rs +3 -3
  14. rxgraph-0.6.0/crates/rxgraph-py/Cargo.toml +0 -23
  15. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/benches/flight_routes.rs +0 -0
  16. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/benches/memory.rs +0 -0
  17. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/benches/payment_risk.rs +0 -0
  18. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/examples/flight_routes.rs +0 -0
  19. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/arrow.rs +0 -0
  20. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/arrow_value.rs +0 -0
  21. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/bind.rs +0 -0
  22. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/eval.rs +0 -0
  23. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/expr.rs +0 -0
  24. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/mod.rs +0 -0
  25. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/list.rs +0 -0
  26. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/mod.rs +0 -0
  27. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/scalar.rs +0 -0
  28. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/string.rs +0 -0
  29. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/struct_.rs +0 -0
  30. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/polars_json.rs +0 -0
  31. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/value.rs +0 -0
  32. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/examples/kernels.rs +0 -0
  33. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/examples/mod.rs +0 -0
  34. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/csr.rs +0 -0
  35. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/graph.rs +0 -0
  36. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/mod.rs +0 -0
  37. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/repo.rs +0 -0
  38. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/algo.rs +0 -0
  39. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/config.rs +0 -0
  40. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/kernel.rs +0 -0
  41. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/progress.rs +0 -0
  42. {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/registry.rs +0 -0
  43. {rxgraph-0.6.0 → rxgraph-0.7.0}/pyproject.toml +0 -0
  44. {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/__init__.py +0 -0
  45. {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/__init__.pyi +0 -0
  46. {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/_graph_tables.py +0 -0
  47. {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/plugin.py +0 -0
  48. {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/py.typed +0 -0
@@ -1302,7 +1302,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
1302
1302
 
1303
1303
  [[package]]
1304
1304
  name = "rxgraph"
1305
- version = "0.6.0"
1305
+ version = "0.7.0"
1306
1306
  dependencies = [
1307
1307
  "anyhow",
1308
1308
  "arrow",
@@ -1310,6 +1310,8 @@ dependencies = [
1310
1310
  "criterion",
1311
1311
  "inventory",
1312
1312
  "pretty_assertions",
1313
+ "pyo3",
1314
+ "pyo3-arrow",
1313
1315
  "rand",
1314
1316
  "rayon",
1315
1317
  "serde",
@@ -1319,25 +1321,12 @@ dependencies = [
1319
1321
  "stats_alloc",
1320
1322
  ]
1321
1323
 
1322
- [[package]]
1323
- name = "rxgraph-py"
1324
- version = "0.5.0"
1325
- dependencies = [
1326
- "anyhow",
1327
- "arrow",
1328
- "pyo3",
1329
- "pyo3-arrow",
1330
- "rayon",
1331
- "rxgraph",
1332
- "serde_json",
1333
- ]
1334
-
1335
1324
  [[package]]
1336
1325
  name = "rxgraph-python"
1337
- version = "0.6.0"
1326
+ version = "0.7.0"
1338
1327
  dependencies = [
1339
1328
  "pyo3",
1340
- "rxgraph-py",
1329
+ "rxgraph",
1341
1330
  ]
1342
1331
 
1343
1332
  [[package]]
@@ -1,5 +1,5 @@
1
1
  [workspace]
2
- members = ["crates/rxgraph", "crates/rxgraph-py", "crates/rxgraph-python"]
2
+ members = ["crates/rxgraph", "crates/rxgraph-python"]
3
3
  exclude = ["examples/rust-kernel-plugin"]
4
4
  resolver = "3"
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rxgraph
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -185,10 +185,20 @@ result = graph.search(
185
185
  )
186
186
  ```
187
187
 
188
+ The consumer's crate depends on `rxgraph` with the `python` feature (their own
189
+ crate is the `cdylib` that needs pyo3's `extension-module`; rxgraph's `python`
190
+ feature deliberately does not enable it):
191
+
192
+ ```toml
193
+ [dependencies]
194
+ rxgraph = { version = "0.6", features = ["python"] }
195
+ pyo3 = { version = "0.28", features = ["extension-module"] }
196
+ ```
197
+
188
198
  The Rust side implements `rxgraph::Kernel` and ends with a small macro call:
189
199
 
190
200
  ```rust
191
- rxgraph_py::plugin! {
201
+ rxgraph::plugin! {
192
202
  module = _native;
193
203
  "hop_budget" => HopBudget::from_params,
194
204
  }
@@ -171,10 +171,20 @@ result = graph.search(
171
171
  )
172
172
  ```
173
173
 
174
+ The consumer's crate depends on `rxgraph` with the `python` feature (their own
175
+ crate is the `cdylib` that needs pyo3's `extension-module`; rxgraph's `python`
176
+ feature deliberately does not enable it):
177
+
178
+ ```toml
179
+ [dependencies]
180
+ rxgraph = { version = "0.6", features = ["python"] }
181
+ pyo3 = { version = "0.28", features = ["extension-module"] }
182
+ ```
183
+
174
184
  The Rust side implements `rxgraph::Kernel` and ends with a small macro call:
175
185
 
176
186
  ```rust
177
- rxgraph_py::plugin! {
187
+ rxgraph::plugin! {
178
188
  module = _native;
179
189
  "hop_budget" => HopBudget::from_params,
180
190
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rxgraph"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  edition.workspace = true
5
5
  rust-version.workspace = true
6
6
  description = "High-performance graph traversal engine"
@@ -22,6 +22,11 @@ anyhow.workspace = true
22
22
  arrow.workspace = true
23
23
  arrow-schema.workspace = true
24
24
  inventory.workspace = true
25
+ # Optional: only pulled in by the `python` feature for the PyO3 binding layer.
26
+ # `extension-module` is intentionally NOT enabled here; the final extension
27
+ # crate (cdylib) enables it, so this stays linkable/testable as an rlib.
28
+ pyo3 = { workspace = true, optional = true }
29
+ pyo3-arrow = { workspace = true, optional = true }
25
30
  rand.workspace = true
26
31
  rayon.workspace = true
27
32
  serde.workspace = true
@@ -30,6 +35,12 @@ smallvec.workspace = true
30
35
  sprs.workspace = true
31
36
  stats_alloc.workspace = true
32
37
 
38
+ [features]
39
+ # Exposes the PyO3 binding layer (`register`, the `plugin!` macro, and pyclasses)
40
+ # so downstream consumers can build native kernel plugins against the published
41
+ # crate without an unpublished path-dependency.
42
+ python = ["dep:pyo3", "dep:pyo3-arrow"]
43
+
33
44
  [dev-dependencies]
34
45
  criterion.workspace = true
35
46
  pretty_assertions.workspace = true
@@ -21,7 +21,7 @@ evaluates stateful traversal kernels without copying user columns out of Arrow.
21
21
 
22
22
  ```toml
23
23
  [dependencies]
24
- rxgraph = "0.0.5"
24
+ rxgraph = "0.6"
25
25
  ```
26
26
 
27
27
  ## Data Model
@@ -121,8 +121,8 @@ parallelism and optional per-node intermediate state materialization.
121
121
 
122
122
  ## Python Bindings
123
123
 
124
- Python bindings are published separately as the `rxgraph` package on PyPI from
125
- the same repository. The reusable PyO3 binding layer is also available as the
126
- `rxgraph-py` crate. Downstream native-kernel plugins use
127
- `rxgraph_py::plugin!` to register kernels and build a custom extension; see
124
+ Python bindings are published as the `rxgraph` package on PyPI. The same PyO3
125
+ binding layer is available from this crate via the `python` feature; downstream
126
+ native-kernel plugins enable `features = ["python"]` and use the
127
+ `rxgraph::plugin!` macro to register kernels and build a custom extension; see
128
128
  [`examples/rust-kernel-plugin/`](../../examples/rust-kernel-plugin/README.md).
@@ -0,0 +1,378 @@
1
+ use std::{
2
+ cell::RefCell,
3
+ collections::{BTreeMap, BTreeSet},
4
+ sync::OnceLock,
5
+ };
6
+
7
+ use anyhow::{Context, Result};
8
+ use rxgraph::{
9
+ GraphId, NodeId, RunOptions, TraversalStrategy, search_native,
10
+ traversal::native::{self, GraphStore, OutgoingEdge},
11
+ };
12
+
13
+ fn main() -> Result<()> {
14
+ let store = FraudStore::sample();
15
+ let result = run_search(&store, false)?;
16
+
17
+ println!(
18
+ "paths={} evaluated_edges={} loaded_nodes={:?} loaded_edges={:?} loaded_outgoing={:?}",
19
+ result.paths.len(),
20
+ result.stats.evaluated_edges,
21
+ store.loaded_nodes(),
22
+ store.loaded_edges(),
23
+ store.loaded_outgoing(),
24
+ );
25
+ for path in result.paths {
26
+ let labels = path
27
+ .nodes
28
+ .iter()
29
+ .map(|node| node.payload.label)
30
+ .collect::<Vec<_>>();
31
+ println!(
32
+ "path labels={labels:?} risk={} checkpoints={:?}",
33
+ path.state.total_risk, path.state.checkpoints
34
+ );
35
+ }
36
+
37
+ Ok(())
38
+ }
39
+
40
+ fn run_search(
41
+ store: &FraudStore,
42
+ intermediate_states: bool,
43
+ ) -> Result<native::SearchResult<'_, Account, Transfer, RiskState>> {
44
+ search_native(
45
+ store,
46
+ RiskKernel {
47
+ max_risk: 7,
48
+ require_checkpoint: true,
49
+ },
50
+ RunOptions {
51
+ start_nodes: vec![0_u64.into()],
52
+ strategy: TraversalStrategy::BreadthFirst,
53
+ intermediate_states,
54
+ ..RunOptions::default()
55
+ },
56
+ )
57
+ }
58
+
59
+ #[derive(Clone, Debug, PartialEq, Eq)]
60
+ struct Account {
61
+ label: &'static str,
62
+ target: bool,
63
+ blocked: bool,
64
+ checkpoint: bool,
65
+ }
66
+
67
+ #[derive(Clone, Debug, PartialEq, Eq)]
68
+ struct Transfer {
69
+ risk: u64,
70
+ allowed: bool,
71
+ }
72
+
73
+ #[derive(Clone, Debug)]
74
+ struct TransferRow {
75
+ src: NodeId,
76
+ dest: NodeId,
77
+ transfer: Transfer,
78
+ }
79
+
80
+ #[derive(Clone, Debug, PartialEq, Eq)]
81
+ struct RiskState {
82
+ total_risk: u64,
83
+ visited: BTreeSet<NodeId>,
84
+ checkpoints: BTreeMap<NodeId, &'static str>,
85
+ }
86
+
87
+ #[derive(Clone, Debug)]
88
+ struct RiskKernel {
89
+ max_risk: u64,
90
+ require_checkpoint: bool,
91
+ }
92
+
93
+ impl native::Kernel for RiskKernel {
94
+ type Node = Account;
95
+ type Edge = Transfer;
96
+ type State = RiskState;
97
+
98
+ fn initial_state(
99
+ &self,
100
+ cx: &native::StartCtx<'_, Self::Node, Self::Edge>,
101
+ ) -> Result<Self::State> {
102
+ let account = cx.node()?;
103
+ let mut visited = BTreeSet::new();
104
+ visited.insert(cx.id());
105
+ let mut checkpoints = BTreeMap::new();
106
+ if account.checkpoint {
107
+ checkpoints.insert(cx.id(), account.label);
108
+ }
109
+ Ok(RiskState {
110
+ total_risk: 0,
111
+ visited,
112
+ checkpoints,
113
+ })
114
+ }
115
+
116
+ fn visit(&self, cx: &native::EdgeCtx<'_, Self::Node, Self::Edge, Self::State>) -> Result<bool> {
117
+ let transfer = cx.edge()?;
118
+ let dest = cx.dest()?;
119
+ Ok(transfer.allowed
120
+ && !dest.blocked
121
+ && cx.state().total_risk.saturating_add(transfer.risk) <= self.max_risk)
122
+ }
123
+
124
+ fn next_state(
125
+ &self,
126
+ cx: &native::EdgeCtx<'_, Self::Node, Self::Edge, Self::State>,
127
+ ) -> Result<Self::State> {
128
+ let transfer = cx.edge()?;
129
+ let dest = cx.dest()?;
130
+ let mut next = cx.state().clone();
131
+ next.total_risk += transfer.risk;
132
+ next.visited.insert(cx.dest_id());
133
+ if dest.checkpoint {
134
+ next.checkpoints.insert(cx.dest_id(), dest.label);
135
+ }
136
+ Ok(next)
137
+ }
138
+
139
+ fn stop(&self, cx: &native::EdgeCtx<'_, Self::Node, Self::Edge, Self::State>) -> Result<bool> {
140
+ Ok(cx.dest()?.target && (!self.require_checkpoint || !cx.state().checkpoints.is_empty()))
141
+ }
142
+ }
143
+
144
+ struct FraudStore {
145
+ accounts: Vec<Account>,
146
+ transfers: Vec<TransferRow>,
147
+ outgoing_index: Vec<Vec<usize>>,
148
+ account_cache: Vec<OnceLock<Account>>,
149
+ transfer_cache: Vec<OnceLock<Transfer>>,
150
+ outgoing_cache: Vec<OnceLock<Vec<OutgoingEdge>>>,
151
+ account_loads: RefCell<BTreeSet<NodeId>>,
152
+ transfer_loads: RefCell<BTreeSet<u32>>,
153
+ outgoing_loads: RefCell<BTreeSet<NodeId>>,
154
+ }
155
+
156
+ impl FraudStore {
157
+ fn sample() -> Self {
158
+ let accounts = vec![
159
+ Account {
160
+ label: "origin",
161
+ target: false,
162
+ blocked: false,
163
+ checkpoint: false,
164
+ },
165
+ Account {
166
+ label: "merchant",
167
+ target: false,
168
+ blocked: false,
169
+ checkpoint: true,
170
+ },
171
+ Account {
172
+ label: "sink",
173
+ target: true,
174
+ blocked: false,
175
+ checkpoint: false,
176
+ },
177
+ Account {
178
+ label: "cold-wallet",
179
+ target: false,
180
+ blocked: false,
181
+ checkpoint: false,
182
+ },
183
+ Account {
184
+ label: "blocked",
185
+ target: true,
186
+ blocked: true,
187
+ checkpoint: false,
188
+ },
189
+ ];
190
+ let transfers = vec![
191
+ TransferRow {
192
+ src: 0,
193
+ dest: 1,
194
+ transfer: Transfer {
195
+ risk: 2,
196
+ allowed: true,
197
+ },
198
+ },
199
+ TransferRow {
200
+ src: 1,
201
+ dest: 2,
202
+ transfer: Transfer {
203
+ risk: 4,
204
+ allowed: true,
205
+ },
206
+ },
207
+ TransferRow {
208
+ src: 1,
209
+ dest: 4,
210
+ transfer: Transfer {
211
+ risk: 1,
212
+ allowed: true,
213
+ },
214
+ },
215
+ TransferRow {
216
+ src: 3,
217
+ dest: 2,
218
+ transfer: Transfer {
219
+ risk: 1,
220
+ allowed: true,
221
+ },
222
+ },
223
+ ];
224
+ Self::new(accounts, transfers)
225
+ }
226
+
227
+ fn new(accounts: Vec<Account>, transfers: Vec<TransferRow>) -> Self {
228
+ let mut outgoing_index = vec![Vec::new(); accounts.len()];
229
+ for (edge, transfer) in transfers.iter().enumerate() {
230
+ outgoing_index[transfer.src as usize].push(edge);
231
+ }
232
+ let account_cache = (0..accounts.len()).map(|_| OnceLock::new()).collect();
233
+ let transfer_cache = (0..transfers.len()).map(|_| OnceLock::new()).collect();
234
+ let outgoing_cache = (0..accounts.len()).map(|_| OnceLock::new()).collect();
235
+ Self {
236
+ accounts,
237
+ transfers,
238
+ outgoing_index,
239
+ account_cache,
240
+ transfer_cache,
241
+ outgoing_cache,
242
+ account_loads: RefCell::new(BTreeSet::new()),
243
+ transfer_loads: RefCell::new(BTreeSet::new()),
244
+ outgoing_loads: RefCell::new(BTreeSet::new()),
245
+ }
246
+ }
247
+
248
+ fn loaded_nodes(&self) -> BTreeSet<NodeId> {
249
+ self.account_loads.borrow().clone()
250
+ }
251
+
252
+ fn loaded_edges(&self) -> BTreeSet<u32> {
253
+ self.transfer_loads.borrow().clone()
254
+ }
255
+
256
+ fn loaded_outgoing(&self) -> BTreeSet<NodeId> {
257
+ self.outgoing_loads.borrow().clone()
258
+ }
259
+ }
260
+
261
+ impl GraphStore for FraudStore {
262
+ type Node = Account;
263
+ type Edge = Transfer;
264
+
265
+ fn resolve_node(&self, external: GraphId<'_>) -> Result<Option<NodeId>> {
266
+ Ok(match external {
267
+ GraphId::U64(value) if (value as usize) < self.accounts.len() => Some(value as NodeId),
268
+ _ => None,
269
+ })
270
+ }
271
+
272
+ fn external_node(&self, internal: NodeId) -> Result<Option<GraphId<'_>>> {
273
+ Ok(((internal as usize) < self.accounts.len()).then_some(GraphId::U64(internal as u64)))
274
+ }
275
+
276
+ fn external_edge(&self, internal: rxgraph::EdgeId) -> Result<Option<GraphId<'_>>> {
277
+ Ok(((internal as usize) < self.transfers.len()).then_some(GraphId::U64(internal as u64)))
278
+ }
279
+
280
+ fn outgoing(&self, src: NodeId) -> Result<&[OutgoingEdge]> {
281
+ let src_index = src as usize;
282
+ let edge_ids = self
283
+ .outgoing_index
284
+ .get(src_index)
285
+ .with_context(|| format!("node row {src} is out of range"))?;
286
+ let outgoing = self
287
+ .outgoing_cache
288
+ .get(src_index)
289
+ .context("outgoing cache row is missing")?
290
+ .get_or_init(|| {
291
+ self.outgoing_loads.borrow_mut().insert(src);
292
+ edge_ids
293
+ .iter()
294
+ .map(|&edge| OutgoingEdge {
295
+ edge: edge as rxgraph::EdgeId,
296
+ dest: self.transfers[edge].dest,
297
+ })
298
+ .collect()
299
+ });
300
+ Ok(outgoing)
301
+ }
302
+
303
+ fn node(&self, id: NodeId) -> Result<&Self::Node> {
304
+ let index = id as usize;
305
+ let account = self
306
+ .accounts
307
+ .get(index)
308
+ .with_context(|| format!("node row {id} is out of range"))?;
309
+ Ok(self
310
+ .account_cache
311
+ .get(index)
312
+ .context("account cache row is missing")?
313
+ .get_or_init(|| {
314
+ self.account_loads.borrow_mut().insert(id);
315
+ account.clone()
316
+ }))
317
+ }
318
+
319
+ fn edge(&self, id: rxgraph::EdgeId) -> Result<&Self::Edge> {
320
+ let index = id as usize;
321
+ let transfer = self
322
+ .transfers
323
+ .get(index)
324
+ .with_context(|| format!("edge row {id} is out of range"))?;
325
+ Ok(self
326
+ .transfer_cache
327
+ .get(index)
328
+ .context("transfer cache row is missing")?
329
+ .get_or_init(|| {
330
+ self.transfer_loads.borrow_mut().insert(id);
331
+ transfer.transfer.clone()
332
+ }))
333
+ }
334
+ }
335
+
336
+ #[cfg(test)]
337
+ mod tests {
338
+ use pretty_assertions::assert_eq;
339
+
340
+ use super::*;
341
+
342
+ #[test]
343
+ fn native_example_returns_native_state_and_payloads() {
344
+ let store = FraudStore::sample();
345
+ let result = run_search(&store, true).unwrap();
346
+
347
+ assert_eq!(result.paths.len(), 1);
348
+ let path = &result.paths[0];
349
+ assert_eq!(
350
+ path.nodes
351
+ .iter()
352
+ .map(|node| node.payload.label)
353
+ .collect::<Vec<_>>(),
354
+ vec!["origin", "merchant", "sink"]
355
+ );
356
+ assert_eq!(path.state.total_risk, 6);
357
+ assert_eq!(path.state.visited, BTreeSet::from([0, 1, 2]));
358
+ assert_eq!(path.state.checkpoints, BTreeMap::from([(1, "merchant")]));
359
+ assert_eq!(
360
+ path.nodes
361
+ .iter()
362
+ .map(|node| node.state.as_ref().unwrap().total_risk)
363
+ .collect::<Vec<_>>(),
364
+ vec![0, 2, 6]
365
+ );
366
+ }
367
+
368
+ #[test]
369
+ fn native_example_loads_only_reached_rows() {
370
+ let store = FraudStore::sample();
371
+ let result = run_search(&store, false).unwrap();
372
+
373
+ assert_eq!(result.stats.evaluated_edges, 3);
374
+ assert_eq!(store.loaded_outgoing(), BTreeSet::from([0, 1]));
375
+ assert_eq!(store.loaded_nodes(), BTreeSet::from([0, 1, 2, 4]));
376
+ assert_eq!(store.loaded_edges(), BTreeSet::from([0, 1, 2]));
377
+ }
378
+ }
@@ -28,14 +28,19 @@ mod arrow;
28
28
  pub mod dsl;
29
29
  pub mod examples;
30
30
  pub mod graph;
31
+ #[cfg(feature = "python")]
32
+ mod python;
31
33
  pub mod traversal;
32
34
 
35
+ #[cfg(feature = "python")]
36
+ pub use python::register;
37
+
33
38
  pub use dsl::{DslExpr, DslKernel, Scalar, StateRow, Value};
34
39
  pub use graph::{EdgeId, Graph, GraphId, GraphRepo, NodeId, OwnedGraphId};
35
40
  pub use traversal::{
36
41
  BoxedRun, EdgeCtx, GraphPath, Kernel, KernelEntry, RunKernel, RunOptions, SearchResult,
37
42
  SearchStats, TraversalConfig, TraversalConfigBuilder, TraversalStrategy, boxed_run,
38
- build_kernel, register_kernel,
43
+ build_kernel, register_kernel, search_native,
39
44
  };
40
45
 
41
46
  // Re-exported so plugin crates can register kernels without their own dependency.
@@ -1,3 +1,7 @@
1
+ use crate::{
2
+ DslKernel, Graph, GraphId, OwnedGraphId, RunOptions, SearchResult, SearchStats, StateRow,
3
+ TraversalConfigBuilder, TraversalStrategy, Value, build_kernel,
4
+ };
1
5
  use pyo3::{
2
6
  Borrowed,
3
7
  conversion::IntoPyObjectExt,
@@ -7,10 +11,6 @@ use pyo3::{
7
11
  };
8
12
  use pyo3_arrow::PyTable;
9
13
  use rayon::ThreadPoolBuilder;
10
- use rxgraph::{
11
- DslKernel, Graph, GraphId, OwnedGraphId, RunOptions, SearchResult, SearchStats, StateRow,
12
- TraversalConfigBuilder, TraversalStrategy, Value, build_kernel,
13
- };
14
14
  use std::thread;
15
15
 
16
16
  /// Registers all rxgraph native classes and functions into a Python module.
@@ -37,7 +37,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
37
37
  /// `rxgraph::Kernel`.
38
38
  ///
39
39
  /// ```ignore
40
- /// rxgraph_py::plugin! {
40
+ /// rxgraph::plugin! {
41
41
  /// module = _native;
42
42
  /// "hop_budget" => HopBudget::from_params,
43
43
  /// }
@@ -49,10 +49,10 @@ macro_rules! plugin {
49
49
  $($name:literal => $factory:expr),+ $(,)?
50
50
  ) => {
51
51
  $(
52
- ::rxgraph::inventory::submit! {
53
- ::rxgraph::KernelEntry {
52
+ $crate::inventory::submit! {
53
+ $crate::KernelEntry {
54
54
  name: $name,
55
- make: |params| Ok(::rxgraph::boxed_run(($factory)(params)?)),
55
+ make: |params| Ok($crate::boxed_run(($factory)(params)?)),
56
56
  }
57
57
  }
58
58
  )+
@@ -466,7 +466,7 @@ struct PySearchPath {
466
466
  }
467
467
 
468
468
  impl PySearchPath {
469
- fn from_path(_py: Python<'_>, path: rxgraph::GraphPath<'_>) -> PyResult<Self> {
469
+ fn from_path(_py: Python<'_>, path: crate::GraphPath<'_>) -> PyResult<Self> {
470
470
  Ok(Self {
471
471
  nodes: path.nodes.into_iter().map(GraphId::into_owned).collect(),
472
472
  edges: path.edges.into_iter().map(GraphId::into_owned).collect(),
@@ -21,6 +21,7 @@
21
21
  mod algo;
22
22
  mod config;
23
23
  mod kernel;
24
+ pub mod native;
24
25
  mod progress;
25
26
  mod registry;
26
27
 
@@ -29,6 +30,7 @@ use crate::{dsl::StateRow, graph::GraphId};
29
30
  pub use algo::RunOptions;
30
31
  pub use config::{TraversalConfig, TraversalConfigBuilder, TraversalStrategy};
31
32
  pub use kernel::{EdgeCtx, Kernel};
33
+ pub use native::search_native;
32
34
  pub use registry::{
33
35
  BoxedRun, KernelEntry, RunKernel, boxed_run, build_kernel, inventory, register_kernel,
34
36
  };