rgapi 0.1.23__tar.gz → 0.1.25__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.
@@ -29,7 +29,7 @@ jobs:
29
29
  - uses: actions/checkout@v7
30
30
  - uses: PyO3/maturin-action@v1
31
31
  with:
32
- args: --release --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13
32
+ args: --profile dist --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13
33
33
  manylinux: auto
34
34
  - uses: actions/upload-artifact@v7
35
35
  with:
@@ -28,6 +28,12 @@ version = "1.0.4"
28
28
  source = "registry+https://github.com/rust-lang/crates.io-index"
29
29
  checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
30
30
 
31
+ [[package]]
32
+ name = "core_detect"
33
+ version = "1.0.0"
34
+ source = "registry+https://github.com/rust-lang/crates.io-index"
35
+ checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
36
+
31
37
  [[package]]
32
38
  name = "crc32fast"
33
39
  version = "1.5.1"
@@ -39,9 +45,9 @@ dependencies = [
39
45
 
40
46
  [[package]]
41
47
  name = "crossbeam-deque"
42
- version = "0.8.7"
48
+ version = "0.8.8"
43
49
  source = "registry+https://github.com/rust-lang/crates.io-index"
44
- checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
50
+ checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
45
51
  dependencies = [
46
52
  "crossbeam-epoch",
47
53
  "crossbeam-utils",
@@ -49,26 +55,32 @@ dependencies = [
49
55
 
50
56
  [[package]]
51
57
  name = "crossbeam-epoch"
52
- version = "0.9.20"
58
+ version = "0.9.21"
53
59
  source = "registry+https://github.com/rust-lang/crates.io-index"
54
- checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
60
+ checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
55
61
  dependencies = [
56
62
  "crossbeam-utils",
57
63
  ]
58
64
 
59
65
  [[package]]
60
66
  name = "crossbeam-utils"
61
- version = "0.8.22"
67
+ version = "0.8.23"
62
68
  source = "registry+https://github.com/rust-lang/crates.io-index"
63
- checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
69
+ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
64
70
 
65
71
  [[package]]
66
72
  name = "encoding_rs"
67
- version = "0.8.35"
73
+ version = "0.8.40"
68
74
  source = "registry+https://github.com/rust-lang/crates.io-index"
69
- checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
75
+ checksum = "2a7a45518d2863d18aa47f4a0cf9faec2aa4304cc09df5e41299f276b3ad135e"
70
76
  dependencies = [
71
77
  "cfg-if",
78
+ "core_detect",
79
+ "multiversion",
80
+ "multiversion_no_op",
81
+ "rustversion",
82
+ "scopeguard",
83
+ "simdutf8",
72
84
  ]
73
85
 
74
86
  [[package]]
@@ -185,6 +197,34 @@ dependencies = [
185
197
  "libc",
186
198
  ]
187
199
 
200
+ [[package]]
201
+ name = "multiversion"
202
+ version = "0.8.0"
203
+ source = "registry+https://github.com/rust-lang/crates.io-index"
204
+ checksum = "7edb7f0ff51249dfda9ab96b5823695e15a052dc15074c9dbf3d118afaf2c201"
205
+ dependencies = [
206
+ "multiversion-macros",
207
+ "target-features",
208
+ ]
209
+
210
+ [[package]]
211
+ name = "multiversion-macros"
212
+ version = "0.8.0"
213
+ source = "registry+https://github.com/rust-lang/crates.io-index"
214
+ checksum = "b093064383341eb3271f42e381cb8f10a01459478446953953c75d24bd339fc0"
215
+ dependencies = [
216
+ "proc-macro2",
217
+ "quote",
218
+ "syn 2.0.119",
219
+ "target-features",
220
+ ]
221
+
222
+ [[package]]
223
+ name = "multiversion_no_op"
224
+ version = "1.0.0"
225
+ source = "registry+https://github.com/rust-lang/crates.io-index"
226
+ checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
227
+
188
228
  [[package]]
189
229
  name = "once_cell"
190
230
  version = "1.21.4"
@@ -291,7 +331,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
291
331
 
292
332
  [[package]]
293
333
  name = "rgapi"
294
- version = "0.1.23"
334
+ version = "0.1.25"
295
335
  dependencies = [
296
336
  "crc32fast",
297
337
  "globset",
@@ -304,6 +344,12 @@ dependencies = [
304
344
  "serde_json",
305
345
  ]
306
346
 
347
+ [[package]]
348
+ name = "rustversion"
349
+ version = "1.0.23"
350
+ source = "registry+https://github.com/rust-lang/crates.io-index"
351
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
352
+
307
353
  [[package]]
308
354
  name = "same-file"
309
355
  version = "1.0.6"
@@ -313,6 +359,12 @@ dependencies = [
313
359
  "winapi-util",
314
360
  ]
315
361
 
362
+ [[package]]
363
+ name = "scopeguard"
364
+ version = "1.2.0"
365
+ source = "registry+https://github.com/rust-lang/crates.io-index"
366
+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
367
+
316
368
  [[package]]
317
369
  name = "serde"
318
370
  version = "1.0.229"
@@ -340,7 +392,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
340
392
  dependencies = [
341
393
  "proc-macro2",
342
394
  "quote",
343
- "syn 3.0.4",
395
+ "syn 3.0.5",
344
396
  ]
345
397
 
346
398
  [[package]]
@@ -356,6 +408,12 @@ dependencies = [
356
408
  "zmij",
357
409
  ]
358
410
 
411
+ [[package]]
412
+ name = "simdutf8"
413
+ version = "0.1.5"
414
+ source = "registry+https://github.com/rust-lang/crates.io-index"
415
+ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
416
+
359
417
  [[package]]
360
418
  name = "syn"
361
419
  version = "2.0.119"
@@ -369,15 +427,21 @@ dependencies = [
369
427
 
370
428
  [[package]]
371
429
  name = "syn"
372
- version = "3.0.4"
430
+ version = "3.0.5"
373
431
  source = "registry+https://github.com/rust-lang/crates.io-index"
374
- checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
432
+ checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
375
433
  dependencies = [
376
434
  "proc-macro2",
377
435
  "quote",
378
436
  "unicode-ident",
379
437
  ]
380
438
 
439
+ [[package]]
440
+ name = "target-features"
441
+ version = "0.1.6"
442
+ source = "registry+https://github.com/rust-lang/crates.io-index"
443
+ checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5"
444
+
381
445
  [[package]]
382
446
  name = "target-lexicon"
383
447
  version = "0.13.5"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rgapi"
3
- version = "0.1.23"
3
+ version = "0.1.25"
4
4
  edition = "2024"
5
5
  rust-version = "1.91"
6
6
  license = "Apache-2.0"
@@ -33,4 +33,18 @@ extension-module = ["python", "pyo3/extension-module"]
33
33
  too_many_arguments = "allow"
34
34
 
35
35
  [profile.release]
36
+ lto = false
37
+ codegen-units = 16
38
+
39
+ [profile.release.package.rgapi]
40
+ incremental = true
41
+
42
+ [profile.dist]
43
+ inherits = "release"
44
+ lto = true
45
+ incremental = false
46
+ codegen-units = 1
36
47
  strip = true
48
+
49
+ [profile.dist.package.rgapi]
50
+ incremental = false
@@ -9,7 +9,7 @@ src/walk.rs ignore/globset/grep-regex-backed path walking and filtering
9
9
  src/search.rs grep-regex/grep-searcher-backed searching
10
10
  src/block.rs blank-line-delimited block grouping, matching, and block context
11
11
  src/python.rs PyO3 classes and private core functions
12
- python/rgapi/ public Python wrappers over `rgapi._core`
12
+ python/rgapi/ public Python wrappers over `rgapi._core`, plus the `rgapi-nbrg` CLI
13
13
  tests/ pytest coverage for the Python API
14
14
  ```
15
15
 
@@ -40,8 +40,7 @@ The GitHub workflow builds wheels for Python 3.10-3.13 on Linux and macOS and pu
40
40
 
41
41
  Paths in `fd`, `walk`, `rg`, and `rg_iter` results are relative to the requested root and use `/` separators. Traversal uses `ignore::WalkParallel`, so result order is not part of the API contract. Search results are structured rows; collected result lists use rg-style `str()` and notebook display. `SearchLine.lnhash` is computed with the same CRC-32-based line-content hash format as exhash (`lineno|hash|`, low 16 bits of CRC-32 over the line's UTF-8 bytes); `lnhashs=True` only changes row display, not `line_number` or matching behavior. Path regexes filter returned/searched paths; `skip_dir` and `skip_dir_re` prune traversal through `ignore::WalkBuilder::filter_entry`. Depth, size, symlink, filesystem, hidden, and ignore options are direct `ignore::WalkBuilder` settings. `rg_iter` exposes the same parallel search stream that `rg` collects by default; `paths=True` and `count=True` consume that stream with different reducers. Binary files and invalid UTF-8 are skipped for now.
42
42
 
43
- Streaming engine: `walk.rs` owns the generic machinery. `StreamIter<T>` is the worker-thread-plus-bounded-channel iterator (`sync_channel(8192)`, so producers block rather than buffer without limit when a consumer lags), and `spawn_walk` owns the shared scaffold: walker config, panic catching, cancel flag, and worker thread. `rg_iter` (`T = SearchLine`), `block_iter` (`T = SearchBlock`), `nb_iter` (`T = NbCell`), and `find_iter` (`T = String`, the path walk) plug entry closures into that engine. Block search reads each file once, searches it once, groups nonblank lines into blocks, maps matching lines to their blocks, and expands context by block index.
44
- Each `SearchBlock` carries numeric boundaries plus hashes for its first and last source lines. Python keeps both and chooses the displayed address without another file read.
43
+ Streaming engine: `walk.rs` owns the generic machinery. `StreamIter<T>` is the worker-thread-plus-bounded-channel iterator (`sync_channel(8192)`, so producers block rather than buffer without limit when a consumer lags), and `spawn_walk` owns the shared scaffold: walker config, panic catching, cancel flag, and worker thread. `rg_iter` (`T = SearchLine`), `block_iter` (`T = SearchBlock`), `nb_iter` (`T = NbCell`), and `find_iter` (`T = String`, the path walk) plug entry closures into that engine. Block search reads each file once, searches it once, groups nonblank lines into blocks, maps matching lines to their blocks, and expands context by block index. Each `SearchBlock` carries numeric boundaries plus hashes for its first and last source lines. Python keeps both and chooses the displayed address without another file read.
45
44
 
46
45
  Async API: `fda`, `fda_iter`, `rga`, `rga_iter`, `nbrga`, and `nbrga_iter` wrap the corresponding private core operations. `rga(summary=True)` uses `_core.block_search_async`; ordinary `rga` uses `_core.rg_async`. Each collected core function takes a Python callback, runs on Rust threads through the generic `stream_async` helper, and delivers with one GIL attach at the end. Iterator forms use `stream_iter_async` and attach once per batch. The Python side settles an `asyncio.Future` or feeds an `asyncio.Queue` via `loop.call_soon_threadsafe`; no Python thread blocks and `asyncio.to_thread` is not involved. `AsyncHandle.cancel()` sets the same atomic flag used by the Rust iterators.
47
46
 
@@ -49,4 +48,6 @@ Truncation is recorded on collected results: `max_results` sets `stop_reason="ma
49
48
 
50
49
  Path results are `FileEntry` rows: a `str` subclass carrying the walk root, so paths stay plain strings for compatibility while stat info loads lazily (one cached `os.lstat` per entry, read only on attribute access). The wrapping happens at result construction on the Python side; Rust still streams plain strings. `PathResults.__repr__` shows an `ls -l`-style listing capped at `MAX_REPR` rows, so a huge result never stats everything, while `str()` stays one plain path per line. `ls` is `fd` with shell-style defaults (one level, dirs, ignore rules off), re-sorted with `stop_reason` preserved.
51
50
 
51
+ Rust callers can consume a `StreamIter` with `cancel_and_join()` to cancel, drain queued results, and wait for the walk's workers to finish. `Drop` remains nonblocking. Since filesystem calls already in progress must return before joining completes, keep `cancel_and_join()` off async executors.
52
+
52
53
  This package intentionally has no CLI. Python is the interface.
rgapi-0.1.25/PKG-INFO ADDED
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: rgapi
3
+ Version: 0.1.25
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Requires-Dist: fastcore>=1.14.6
7
+ Requires-Dist: fastship>=0.0.12 ; extra == 'dev'
8
+ Requires-Dist: maturin>=1.0,<2.0 ; extra == 'dev'
9
+ Requires-Dist: pytest ; extra == 'dev'
10
+ Provides-Extra: dev
11
+ License-File: LICENSE
12
+ Summary: Python API for ripgrep-style file walking and searching
13
+ Home-Page: https://github.com/AnswerDotAI/rgapi
14
+ Author-email: Jeremy Howard <j@fast.ai>
15
+ License: Apache-2.0
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
18
+ Project-URL: Homepage, https://github.com/AnswerDotAI/rgapi
19
+ Project-URL: Issues, https://github.com/AnswerDotAI/rgapi/issues
20
+ Project-URL: Repository, https://github.com/AnswerDotAI/rgapi
21
+
22
+ # rgapi
23
+
24
+ `rgapi` provides `fd`-style file discovery and `rg`-style text search from Python without starting a shell command.
25
+
26
+ It uses the same `ignore`, `grep-regex`, and `grep-searcher` crates that ripgrep uses for walking, regex matching, and file scanning. Walking and searching run in parallel by default. Most expensive work stays in Rust.
27
+
28
+ ## Overview
29
+
30
+ For common file discovery and search:
31
+
32
+ ```python
33
+ from rgapi import fd, ls, rg, rg_iter
34
+
35
+ fd(".", ext="py", exclude="test_*.py")
36
+ ls("src")
37
+ for row in rg_iter("TODO", ".", include="*.py", context=2): print(row.asdict())
38
+ rg("TODO", ".", ext="py", skip_dir=".venv", paths=True)
39
+ ```
40
+
41
+ For cell-aware search of Jupyter notebooks (see [Notebooks](#notebooks)):
42
+
43
+ ```python
44
+ from rgapi import nbrg
45
+
46
+ nbrg("read_csv", ".", cell_context=1)
47
+ ```
48
+
49
+ Walk and search functions have async versions. Streaming versions yield results as the search finds them. See [Async](#async):
50
+
51
+ ```python
52
+ from rgapi import fda, rga, rga_iter, nbrga, nbrga_iter
53
+
54
+ await rga("TODO", ".", ext="py", timeout_ms=200)
55
+ async for row in rga_iter("TODO", "."): print(row)
56
+ ```
57
+
58
+ Use the lower-level functions to compile a regex, search text or a single file, or walk a directory:
59
+
60
+ ```python
61
+ from rgapi import compile, search_path, search_text, walk
62
+
63
+ matcher = compile("TODO")
64
+ matcher.is_match("TODO")
65
+ matcher.finditer("TODO TODO")
66
+
67
+ walk(".")
68
+ search_text(matcher, "alpha\nTODO\nomega\n", path="memory.txt", context=1)
69
+ search_path(matcher, "src/lib.rs", display_path="src/lib.rs")
70
+ ```
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install rgapi
76
+ ```
77
+
78
+ ## File discovery
79
+
80
+ `fd` and `walk` return slash-separated paths relative to `root`. Pass `root` as a `str` or `pathlib.Path`. The sync and async APIs expand `~` and accept `.`, `./`, and paths containing `..`.
81
+
82
+ Discovery uses the `ignore` crate with ripgrep's default filters. It reads `.gitignore`, `.ignore`, and `.rgignore` files. `.rgignore` takes precedence over `.gitignore`. Pass `ignore=False` to disable all ignore-file filtering, including `.rgignore`.
83
+
84
+ Hidden files are skipped unless `hidden=True`. Symlinks are followed only with `follow_links=True`. Use `same_file_system=True` to avoid crossing filesystem boundaries.
85
+
86
+ Traversal runs in parallel without guaranteed result order. Use `sorted(...)` when order matters.
87
+
88
+ `fd` adds filename filters to `walk`. Its `pattern` is a smart-case regex matched against each basename. Lowercase patterns match case-insensitively. A pattern containing uppercase letters is case-sensitive. Use `path_re` to match the slash-separated relative path instead.
89
+
90
+ `include` and `exclude` use glob syntax. `glob=` is an alias for `include=`. A basename glob such as `*.py` also matches nested paths such as `src/app.py`.
91
+
92
+ Filter extensions with `ext="py"` or `ext=["py", "rs"]`. Extension and glob filters must both match. For example, `include="src/*", ext="py"` requires `src/*` and `*.py`, like combining `rg -g` with `-t`.
93
+
94
+ Set `min_depth` and `max_depth` to bound recursion. `max_filesize` skips files above a byte limit.
95
+
96
+ `ls` follows the shell command's listing conventions. It uses `fd` with `max_depth=1`, includes directories, disables ignore rules, and sorts by name. Set `hidden=True` for `ls -a` behaviour. All `fd` filters remain available.
97
+
98
+ `fd_iter` yields `FileEntry` paths as the walk finds them. It accepts every `fd` filter. Stopping iteration ends the walk. It does not accept `timeout_ms`.
99
+
100
+ `path_re` and `skip_path_re` filter slash-separated relative paths using regexes. They select returned paths or searched files without changing traversal. To skip entire subtrees, use `skip_dir` with a glob or `skip_dir_re` with a regex.
101
+
102
+ ## Text search
103
+
104
+ `rg` and `rg_iter` return structured `SearchLine` rows. They accept the same filters as `fd`: `include`, `exclude`, `glob`, `ext`, `path_re`, `skip_path_re`, `skip_dir`, `skip_dir_re`, `min_depth`, `max_depth`, `max_filesize`, `follow_links`, and `same_file_system`.
105
+
106
+ Search is case-sensitive by default, matching `rg`. Use `smart_case=True` for `rg --smart-case` behaviour. Use `case_sensitive=False` to force case-insensitive matching.
107
+
108
+ Each `SearchLine` has these fields:
109
+
110
+ ```text
111
+ kind 'match', 'before', 'after', or 'context'
112
+ path path relative to root
113
+ line_number 1-based line number
114
+ lnhash exhash-style `lineno|hash|` address for the line
115
+ line line text without the trailing newline
116
+ matches list of (start, end) byte offsets for match rows
117
+ ```
118
+
119
+ `rg`, `search_text`, and `search_path` return `SearchResults` by default. This list subclass displays as rg-style multiline text in `str()` and notebook pretty output. `rg_iter` yields rows lazily.
120
+
121
+ `SearchLine` has a structured `repr` and an rg-style `str`. The string display truncates `line` to 180 characters with a trailing `…`. `repr` and `asdict()` retain the full line. `SearchLine.asdict()` returns the fields as a plain Python dictionary.
122
+
123
+ Pass `lnhashs=True` to `rg` or `rg_iter` to display hash addresses instead of line numbers. The `line_number` field remains available.
124
+
125
+ For other result forms, use `rg(..., paths=True)` to return unique matched paths or `rg(..., count=True)` to count match spans. `paths` and `count` cannot both be set.
126
+
127
+ ### Path results
128
+
129
+ `fd`, `walk`, and `ls` return `PathResults`. So do `rg` and `nbrg` with `paths=True`. This is a list of `FileEntry` rows.
130
+
131
+ A `FileEntry` is a `str` subclass containing a relative path. Its `stat` property calls `os.lstat` on first access and caches the result. It returns `None` if the path has vanished. `size`, `mtime`, and `is_dir` use the cached stat result. The object also supports ordinary string operations.
132
+
133
+ `PathResults` displays as an `ls -l`-style listing of at most `rgapi.MAX_REPR` rows. A final `… N more` line reports omitted rows. Only displayed rows need stat calls. `str()` returns one plain path per line. `list(res)` also displays plain paths.
134
+
135
+ ### Limits and timeouts
136
+
137
+ Set `timeout_ms` on `rg`, `fd`, `walk`, or `ls` to stop at a deadline and return the results collected so far. Their async versions accept it too. Results report why the operation stopped:
138
+
139
+ - `stop_reason=None` means the result is complete.
140
+ - `stop_reason="max_results"` means `max_results` truncated the result.
141
+ - `stop_reason="timeout"` means the deadline was reached.
142
+
143
+ `complete` is true exactly when `stop_reason` is `None`. `count=True` returns a plain integer without a completion flag. It cannot be combined with `timeout_ms`.
144
+
145
+ ### Context lines
146
+
147
+ `before_context`, `after_context`, and `context` correspond to `rg -B`, `rg -A`, and `rg -C`. Files containing NUL bytes or invalid UTF-8 are skipped.
148
+
149
+ ### Block summaries
150
+
151
+ `rg(..., summary=True)` returns one row per blank-line-delimited block instead of one row per matching line. Empty and whitespace-only lines delimit blocks. A block containing several matching lines appears once and keeps every matching `SearchLine` in `matches`.
152
+
153
+ ```python
154
+ rg("TODO", ".", summary=True, context=1, maxlen=120)
155
+ ```
156
+
157
+ The result is `BlockResults`, a list of `SearchBlock` objects. Each block has `path`, `block_index`, `start_line`, `end_line`, `start_lnhash`, `end_lnhash`, `kind`, full `source`, and `matches`.
158
+
159
+ Matches display as `path:start-end:source`. Context displays as `path:start-end-source`. With `lnhashs=True`, the range uses copyable boundary addresses such as `path:4|a3f2|,6|b1c3|:source`. Newline runs display as `¶`. `maxlen` limits the displayed text without changing `source` or `asdict()`.
160
+
161
+ In summary mode, `before_context`, `after_context`, and `context` count neighbouring blocks. `max_results` counts matching blocks and retains their context. `summary=True` cannot be combined with `paths` or `count`. It can be combined with `lnhash` for copyable block boundaries.
162
+
163
+ ## Notebooks
164
+
165
+ `nbrg` searches cell source in Jupyter `.ipynb` files and returns matching cells. Each result identifies the cell by its nbformat cell/message id, which stays stable across edits.
166
+
167
+ Plain `rg` searches escaped notebook JSON, including outputs and metadata. Its line numbers refer to that JSON file. `nbrg` searches the reconstructed cell source and identifies the cell you would edit.
168
+
169
+ ```python
170
+ from rgapi import nbrg
171
+
172
+ nbrg("read_csv", ".") # cells whose source matches, across all notebooks under "."
173
+ nbrg("read_csv", ".", cell_context=1) # also include neighbouring cells as context
174
+ ```
175
+
176
+ Notebook discovery, parsing, and matching run together in one parallel Rust pass. Matching uses `rg`'s regex engine with the same `case_sensitive` and `smart_case` behaviour. `nbrg` accepts the discovery filters from `fd` and `rg`, including `include`, `exclude`, `glob`, `hidden`, `max_depth`, and `skip_dir`.
177
+
178
+ `nbrg` returns `NbResults`, a list of `NbCell`. Each `NbCell` has:
179
+
180
+ ```text
181
+ path notebook path relative to root
182
+ cell_index 0-based position of the cell in the notebook
183
+ cell_id nbformat cell id (falls back to the cell index for notebooks without ids)
184
+ cell_type 'code', 'markdown', or 'raw'
185
+ kind 'match' or 'context'
186
+ source full cell source
187
+ matches list of SearchLine rows for the matched lines within the cell
188
+ ```
189
+
190
+ `NbCell.asdict()` returns these fields as a plain dictionary. Its `matches` field contains `SearchLine` dictionaries.
191
+
192
+ `str()` and pretty display show one line per cell. Matches use `path:cell_id:source`. Context uses `path:cell_id-source`. Newline runs display as `¶`.
193
+
194
+ A matching cell's display starts at its first matched line. Earlier lines are replaced by `…[Ln]`, where `n` is the matched line's one-based number within the cell. A leading `#|` directive is retained, as in `#| export…[L4]needle here`.
195
+
196
+ `maxlen` limits displayed source and defaults to 120. The full source remains in `source` and `asdict()`. Each cell appears once even when it has multiple matches. All hits remain in `matches`.
197
+
198
+ `cell_context=N` includes the `N` cells before and after each match as `kind="context"` rows. Context cells are deduplicated within each notebook.
199
+
200
+ `nbrg_iter` yields `NbCell` rows as notebooks are parsed. For collected results, `nbrg` accepts these limits and result options:
201
+
202
+ - `max_results` returns at most that many cells after sorting by path and cell index.
203
+ - `count=True` returns the number of matching cells.
204
+ - `timeout_ms` applies a deadline with the same `stop_reason` values as `rg`.
205
+
206
+ The parser reads only each cell's `id`, `cell_type`, and `source`. It skips outputs and metadata without loading embedded images or plots. `search_nb(pattern, path, ...)` searches a single notebook file in the same way.
207
+
208
+ `rgapi-nbrg` exposes notebook search without requiring a Python kernel:
209
+
210
+ ```bash
211
+ rgapi-nbrg 'read_csv' .
212
+ rgapi-nbrg 'read_csv' . --cell-context 1
213
+ rgapi-nbrg 'read_csv' nbs --glob '*.ipynb' --max-results 20
214
+ ```
215
+
216
+ Run `rgapi-nbrg --help` for its discovery, matching, and output options.
217
+
218
+ ## Async
219
+
220
+ `fda`, `rga`, and `nbrga` are async versions of `fd`, `rg`, and `nbrg`. The async generators `fda_iter`, `rga_iter`, and `nbrga_iter` yield rows as the search finds them. Async functions accept the same arguments and return the same types as their synchronous equivalents.
221
+
222
+ ```python
223
+ from rgapi import fda, rga, rga_iter
224
+
225
+ await fda(".", ext="py")
226
+ res = await rga("TODO", ".", timeout_ms=200)
227
+ if not res.complete: print(f"partial results: {res.stop_reason}")
228
+ async for row in rga_iter("TODO", "."): ...
229
+ ```
230
+
231
+ Walking and searching use Rust threads, without `asyncio.to_thread` or the event loop's executor. A callback uses `loop.call_soon_threadsafe` to complete the awaited future or supply results to the generator's queue. The event loop remains unblocked, with normal `contextvars` behaviour.
232
+
233
+ Cancellation stops the Rust workers within about one row. This includes timeouts from `asyncio.wait_for` or `asyncio.timeout`. It also includes task cancellation, such as starlette cancelling a disconnected client's request.
234
+
235
+ Wrap an async iterator in `contextlib.aclosing` when leaving its loop early. `break` alone delays generator finalization until garbage collection. The context manager provides prompt cleanup:
236
+
237
+ ```python
238
+ async with aclosing(rga_iter("TODO", ".")) as it:
239
+ async for row in it:
240
+ if enough(row): break
241
+ ```
242
+
243
+ Use streaming results for incremental display, such as sending batches to a browser as they arrive. Collected results with `timeout_ms` return what was found before the deadline. Use `asyncio.wait_for` when a timeout should raise instead.
244
+
245
+
246
+ ## Benchmarks
247
+
248
+ `tools/bench.py` compares the `rg` CLI with in-process `rgapi`. Run it against a release build. One run on this machine, using best time from seven repeats:
249
+
250
+ | fixture | rg | rgapi |
251
+ | --- | ---: | ---: |
252
+ | 6 x 2 MB files, 2 matches | 6.54 ms | 1.44 ms |
253
+ | 800 x 1.5 KB files, 2 matches | 13.90 ms | 10.94 ms |
254
+ | tiny dir, repeated 30x | 5.92 ms | 2.14 ms |
255
+