Saltearse al contenido

Python

El paquete namidb de Python es un wrapper de PyO3 / maturin sobre el motor en Rust. El trabajo pesado pasa en Rust; la superficie Python es delgada y explícita.

Consulta Instalación para la matriz completa de instalación y el requisito de pyarrow >= 14.

Client(uri)

import namidb
client = namidb.Client("memory://demo")

La URI selecciona el backend (consulta Backends de almacenamiento para la gramática completa). La primera llamada por proceso arranca un runtime de tokio que es propiedad del cliente.

El parseo de URI delega en la misma implementación Rust que usan la CLI y el servidor. URIs mal formadas lanzan PyValueError; los fallos de inicialización del backend lanzan PyRuntimeError.

Dos caminos de escritura

Cypher (auto-commit)

client.cypher("CREATE (a:Person {name: 'Alice', age: 30})")
client.cypher("CREATE (a:Person {name: 'Bob', age: 25})")
result = client.cypher(
"MATCH (p:Person) WHERE p.age > $min RETURN p.name AS name, p.age AS age",
params={"min": 26},
)
print(result.columns) # ['name', 'age']
print(len(result)) # 1
print(result.first()) # {'name': 'Alice', 'age': 30}
for row in result.rows():
print(row)

Las escrituras Cypher (CREATE, SET, DELETE, MERGE, REMOVE) se persisten antes de que retorne cypher() — el executor llama commit_batch() al final de cada plan de escritura. Todavía conviene llamar client.flush() periódicamente para empujar el memtable a SSTs L0, pero la durabilidad ya está cubierta.

API de staging tipada

import uuid
import namidb
client = namidb.Client("memory://demo")
alice = str(uuid.uuid7())
bob = str(uuid.uuid7())
client.upsert_node("Person", alice, {"name": "Alice", "age": 30})
client.upsert_node("Person", bob, {"name": "Bob"})
client.upsert_edge("KNOWS", alice, bob, {"since": 2020})
client.commit() # WAL + manifest CAS
client.flush() # memtable -> L0 SSTs

La API de staging acumula mutaciones en el batch actual y espera un client.commit() explícito. Pásalo junto con client.flush() cuando quieras los SSTs L0 en disco / en el bucket de inmediato.

Los tombstones usan la misma forma: client.tombstone_node(label, id), client.tombstone_edge(edge_type, src, dst).

Inserts en bulk

Para miles de nodos / aristas en un round-trip, prefiere merge_nodes / merge_edges — agrupan muchas escrituras detrás de un solo round-trip de runtime de tokio + mutex:

client.merge_nodes(
"Person",
[{"id": str(uuid.uuid4()), "name": f"p{i}", "age": 20 + i}
for i in range(10_000)],
)
client.merge_edges(
"KNOWS",
[
{"src": "uuid-a", "dst": "uuid-b", "since": 2020},
{"src": "uuid-b", "dst": "uuid-c", "since": 2021},
],
)
client.commit()
client.flush()

Cada fila de nodo necesita un id (UUID string) más properties arbitrarias. Cada fila de arista necesita src + dst (UUID strings) más properties arbitrarias. merge_nodes / merge_edges se quedan en stage en el batch actual — llama commit() para hacerlas persistentes.

Async (acypher)

import asyncio
import namidb
async def main() -> None:
client = namidb.Client("memory://demo")
await client.acypher("CREATE (p:Person {name: 'Alice'})")
result = await client.acypher(
"MATCH (p:Person {name: $name}) RETURN p.name AS name",
params={"name": "Alice"},
)
print(result.rows())
asyncio.run(main())

acypher se monta sobre el bridge tokio de pyo3-async-runtimes. Cada llamada corre sobre el mismo runtime multi-thread de tokio que respalda la API síncrona, así que mezclar las dos desde un solo Client está bien.

Formatos de resultado

QueryResult expone las mismas filas en varias formas. pyarrow >= 14 es una dependencia obligatoria, así que to_arrow() siempre está disponible:

result = client.cypher(
"MATCH (p:Person) RETURN p.name AS name, p.age AS age ORDER BY p.age DESC"
)
result.rows() # list[dict] — fácil para resultados chicos
table = result.to_arrow() # pyarrow.Table
df = result.to_pandas() # pandas.DataFrame (necesita el extra de pandas)
pl_df = result.to_polars() # polars.DataFrame (necesita el extra de polars)

Llamar to_polars() sin el extra de polars lanza un ImportError claro que apunta al comando de instalación.

El orden de columnas sigue la proyección del RETURN del plan parseado, así que RETURN p.name AS name, p.age AS age siempre te da columnas ["name", "age"] — aun cuando no matchee nada.

Para escaneos de toda una label puedes saltarte el round-trip de Cypher:

table = client.scan_label_arrow("Person")
# Columnas: id, label, lsn, schema_version, después la unión de las
# claves de property a lo largo de las vistas escaneadas (claves
# faltantes se rellenan con None).

API directa

Para lookups que no justifican un round-trip de Cypher:

client.lookup_node("Person", alice)
# {'id': '...', 'label': 'Person', 'lsn': 1, 'schema_version': 0,
# 'properties': {'name': 'Alice', 'age': 30}}
client.scan_label("Person")
client.out_edges("KNOWS", alice)
client.cache_stats() # {hit, miss, evict, ...} del SstCache compartido

Mapeo de tipos (Cypher ↔ Python)

Cypher RuntimeValueTipo Python
NullNone
Boolbool
Integerint
Floatfloat
Stringstr
Bytesbytes
Vector(Vec<f32>)list[float]
Listlist
Mapdict[str, ...]
Datedatetime.date
DateTime (UTC, μs)datetime.datetime (tz=UTC)
Node{"_kind": "node", "id", "label", "properties"}
Rel{"_kind": "rel", "edge_type", "src", "dst", "properties"}
Pathlist[Node|Rel] alternando

bool se chequea antes que int para que True / False no se conviertan silenciosamente a Integer(1) / Integer(0).

Siguientes pasos

  • Leer datos — cláusulas de lectura.
  • Escribir datos — cláusulas de escritura y auto-commit.
  • Embedded (Python) — cuándo y por qué embeber vs. hablar con el servidor HTTP.
  • API HTTP — hablar con un namidb-server remoto desde cualquier lugar.