Closed beta
Turn an estate of scattered systems into one queryable, AI-ready data fabric. NRV is a Python SDK for augmented DAGs over data.
Turn scattered instruments, archives and shared drives into one queryable, AI-ready data fabric. NRV is a Python SDK for augmented DAGs over data.
Turn an estate that cannot stop moving into one queryable, AI-ready data fabric. NRV is a Python SDK for augmented DAGs over data.
Turn decades of departmental systems into one queryable, AI-ready data fabric, inside your own tenant. NRV is a Python SDK for augmented DAGs over data.
Declare and Run
Declare where data lives, how it transforms, and where results land. NRV watches every source and propagates changes through the graph. No scheduler, no orchestration layer: the graph is the program.
Below is a whole working pipeline. It watches a source, keeps a ranked view in sync, and writes results as they change.
from nrv import NRV, Config, Sink, Source, Transform
sales = Source("configs/sales.json", alias="sales")
top = Transform("SELECT * FROM sales ORDER BY amount DESC LIMIT 10",
sales, alias="top")
with NRV() as nerve:
nerve.run(Sink(Config.from_file("configs/out.json"), top))
One estate, however many silos
A Source is a handle on something external: files,
object stores, databases, warehouses, streams, HTTP and REST APIs.
A Transform is SQL over upstream vertices; each
upstream alias is a table name. Two silos that have never met join
in four lines.
The engine survives run(). Results stay queryable
in the same process.
- NRV
- The fabric. Owns the engine and the catalog, and outlives the run.
- Source
- An external resource being ingested, monitored for events.
- Transform
- Literal SQL over upstream vertices; induces a new vertex.
- Materialize
- Persists every update to an external target. Data keeps flowing downstream.
- Sink
- A terminal vertex, delivering data out of the graph.
- Config
- One JSON-round-trippable description of any resource. Secrets by reference only.
# a watched CSV directory, and a table in Postgres
sales = Source("configs/sales.json", alias="sales")
regions = Source("configs/regions.json", alias="regions")
joined = Transform("""
SELECT s.*, r.name AS region_name
FROM sales s
JOIN regions r ON s.region_id = r.id
""", sales, regions, alias="joined")
revenue = Transform("""
SELECT region_name, SUM(amount) AS revenue
FROM joined
GROUP BY region_name
""", joined, alias="revenue")
out = Sink(Config.from_file("configs/out.json"), revenue)
with NRV() as nerve:
nerve.run(out) # changes flow to the sink
nerve.sql("SELECT * FROM revenue") # the engine outlives the run
Everything is an event
A file modification, a schedule tick, a Kafka message: the graph is an event-propagation network. Batch and streaming are the same program. Transforms fully recompute by default. Sources declaring cursor or append-only semantics flow incremental deltas instead.
Materialize is a structural decorator over any
vertex. It persists every update to an external target, data still
moving downstream. Layer boundaries, restart recovery and medallion
architectures live there. Bronze to gold is a few declarations, not
three pipelines. Each layer is a real table: location, format and
write mode from its Config. It stays queryable inside
the graph.
events = Source(
Config(uri="memory://events",
selection={"cursor": {"column": "id"}},
events={"trigger": "push"}),
alias="events",
)
def layer(name, mode, upstream):
cfg = Config(uri=f"file://{LAYERS / name}",
format={"format": "parquet"},
write={"mode": mode})
return Materialize(cfg, upstream, alias=name)
bronze = layer("bronze", "append", events)
clean = Transform("SELECT * FROM bronze WHERE amount > 0",
bronze, alias="clean")
silver = layer("silver", "append", clean)
agg = Transform("SELECT kind, SUM(amount) AS total "
"FROM silver GROUP BY kind",
silver, alias="agg", incremental=False)
gold = layer("gold", "overwrite", agg)
Three surfaces, three trust levels
How the estate is queried depends on who is asking.
sql() runs in your own process: raw and unvalidated.
Restricting the process owner buys nothing. serve()
exposes the estate over HTTP. There, queries are SELECT-only, table
functions are refused, and references are restricted to the
vertices you expose.
ask() gets exactly the same validation as the HTTP
surface. Model output is untrusted, even in-process.
nerve.sql("SELECT * FROM carrier_safety") # in-process, raw, trusted
nerve.serve(gold, port=8080) # /v1/query, /v1/graph
nerve.ask("which carriers have the worst OOS rate?") # validated
A semantic layer that stays out of the way
The core is deterministic: a graph of literal SQL, reproducible run to run. The semantic layer sits beside it, an authoring and analysis aid. It never executes inside the graph. No language model is ever in the execution path.
Point NRV at a catalog. Every source through it is
fingerprinted, matched to a concept, and monitored for schema
drift. No natural language required. Two silos declaring the same
concept become one queryable idea. The optional plugin adds natural
language on top. propose() never executes: a question
becomes a pipeline only through a human reading the SQL.
nerve = NRV(catalog_path="./.nrv/catalog",
provider=AnthropicProvider())
proposal = nerve.propose("worst carriers by out-of-service rate")
print(proposal.sql) # review it, nothing has run
gold = proposal.as_transform(silver, alias="gold") # into the graph
Install
Python 3.11 and up. The core installs the file drivers and nothing else. Optional drivers import lazily. A missing one raises an error naming the extra to install.
The package index is private during closed beta.
pip install nrv # core: file sources and sinks
pip install "nrv[all]" # + s3, postgres, kafka, http
pip install "nrv[semantic-anthropic]" # + a language-model provider
Closed beta Write to us if you have an estate worth pointing it at.