Metadata-Version: 2.4
Name: graphdb-client
Version: 0.3.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.3.0.** Statements, parameters, results, the error hierarchy and
> graph-value materialisation all work, 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.

**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, and choosing it for a write is an explicit caller decision.

**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.
