Metadata-Version: 2.5
Name: graphdb-client
Version: 0.4.0
Summary: Python client for graphdb — an in-memory graph database with a Cypher subset over HTTP/JSON
Project-URL: Homepage, https://pmuston.github.io/graphdb
Project-URL: Documentation, https://pmuston.github.io/graphdb/guide/
Project-URL: Source, https://github.com/pmuston/graphdb-client-py
Project-URL: Issues, https://github.com/pmuston/graphdb-client-py/issues
Author-email: Paul Muston <paul.muston@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: client,cypher,database,driver,graph,graphdb
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database :: Front-Ends
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == 'pandas'
Description-Content-Type: text/markdown

# graphdb-client

Python client for [graphdb](https://pmuston.github.io/graphdb) — an in-memory
graph database serving a subset of openCypher over HTTP/JSON.

> **Status: 0.4.0.** Statements, parameters, results, the error hierarchy and
> graph-value materialisation all work; `read()` is enforced by the server; and
> the suite runs the server's own value-kind conformance contract.

```bash
pip install graphdb-client
```

## Usage

```python
from graphdb_client import GraphDB, ConstraintViolation

db = GraphDB("http://localhost:8080", token=...)
db.wait_ready(timeout=30)

for rec in db.run("MATCH (p:Person)-[:KNOWS]->(f) WHERE p.city = $city RETURN p, f",
                  city="Berlin"):
    print(rec["p"]["name"], "->", rec["f"]["name"])
```

`rec["p"]` is a `Node`: metadata is attributes, properties are mapping access,
so a property called `labels` cannot shadow the labels.

```python
n = db.run("MATCH (n:Person {name:'A'}) RETURN n").value()
n.id, n.element_id, n.labels      # 1, 'n1', ('Person', 'Staff')
n["name"], dict(n)                # 'A', {'name': 'A', 'age': 30}

p = db.run("MATCH p = (a)-[:KNOWS*2]->(b) RETURN p").value()
len(p), len(p.nodes)              # 2 relationships, 3 nodes
p.start_node["name"], [r.type for r in p]
```

Nodes are hashable and compare by element id, so the same node from two queries
is equal and a `set()` of them deduplicates. Hydration recurses, so graph values
inside `collect()` results, variable-length relationship lists and paths are
materialised too.

Writes report what they changed, and failures are ordinary Python exceptions:

```python
res = db.run("CREATE (p:Person {name: $name}) RETURN p", name="Zoe")
res.stats.nodes_created          # 1

try:
    db.run("MATCH (p:Person {name:'Zoe'}) DELETE p")
except ConstraintViolation as e:
    e.code                       # Neo.ClientError.Schema.ConstraintValidationFailed
```

## Design notes

**No session or transaction object.** graphdb runs one statement per
transaction, so a session layer would wrap a single POST in ceremony. If the
server grows an explicit transaction endpoint, that is when a context manager
arrives — not before.

**Graph values are materialised from the server's `kind` discriminator**, never
by guessing from which keys are present. A user map stays a plain `dict`, which
is what makes the discriminator worth having.

**`read()` is enforced, not just named.** It declares
`X-Graphdb-Access-Mode: read`, and the server — which parses Cypher — refuses a
statement that would write. The client still cannot tell a read from a write on
its own; it no longer has to.

```python
db.read("CREATE (:Oops)")     # ClientError: Neo.ClientError.Statement.AccessMode
db.run("CREATE (:Fine)")      # run() is unguarded, as before
```

That depends on the server honouring the header. A server older than 0.30.0
ignores it and performs the write, so the first `read()` warns when the
`access-mode` feature is missing — construct with
`GraphDB(url, requires=("access-mode",))` to make that fatal instead. A guard
that silently fails to apply is worse than none, because you stop looking.

**Retries are opt-in and read-only.** graphdb writes are not idempotent —
`CREATE` duplicates on re-run — so `run()` never retries. `read()` is the
retrying variant.

**Parameters are bound, never interpolated.** `run(query, **params)` keeps the
safe path the shortest one.

**Loading `.cypher` files is out of scope.** Splitting a multi-statement file
correctly requires the Cypher tokeniser, because a `;` inside a string literal
or a backtick-quoted identifier is not a separator. Use `graphdb import`, which
owns that rule.

## Development

Tests run against a real graphdb server, booted per session on a free port. A
mock would only prove the mock matches my belief about the wire format.

```bash
pip install -e . pytest
pytest                              # skips integration tests without graphdb on PATH
GRAPHDB_REQUIRE_SERVER=1 pytest     # turns that skip into a failure, as CI does
```

The `kind` contract has two implementations — the server that tags graph values
and this client that materialises them — so neither keeps its own copy of it.
`graphdb conformance` prints the cases, embedded in the binary, and the suite
runs whichever binary it is testing against. There is no file to update and no
URL to fetch, so the two cannot disagree about which contract is current. Those
cases need graphdb 0.26.0+; against older servers they skip.

## Compatibility

Targets graphdb interface version 1 (server 0.18.0+, which added the `kind`
discriminator). The client feature-detects on connect against the `features`
list published by `GET /`.

## Licence

MIT.
