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

```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
```

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