# Design: 0.7 Python API
Status: accepted for milestone 0.7
Milestone: 0.7
Related: ADR-017, [`milestones/0.7.md`](../milestones/0.7.md)
## Goal
Ship a **Pythonic** PyPI package `oxiland` over the frozen 0.6 safe Rust facade.
The Python surface teaches itself; it is not “call this Rust method under another
name.”
## Layout
```text
python/ maturin project (not a root workspace member)
Cargo.toml cdylib crate `oxiland` (PyO3)
pyproject.toml
src/ Rust extension sources
oxiland.pyi PEP 561 stub
py.typed
tests/ pytest
examples/
```
Dependency arrow: Python → PyO3 cdylib → path `oxiland` crate → Oxigraph.
Never: Python → `oxiland-capi`.
## Exception hierarchy
| (base) | `OxilandError` |
| `InvalidRdf` | `InvalidRdfError` |
| `Parse` | `ParseError` (attrs: `message`, `location`) |
| `Serialize` | `SerializeError` |
| `SparqlParse` | `SparqlParseError` |
| `SparqlEvaluation` | `SparqlEvaluationError` |
| `Storage` | `StorageError` |
| `Unsupported` | `UnsupportedError` |
| `Io` | `IoError` |
| `OpenStore` | `OpenStoreError` (attrs: `path`, `message`) |
## Public surface sketch
### Terms
`NamedNode(iri)`, `BlankNode(id=None)`, `Literal(value, *, language=None, datatype=None)`,
`Triple(s, p, o)`, `Quad(s, p, o, g=None)`, helpers for default / named graph names.
### Model
```python
Model() # memory
Model.open(path, *, read_only=False, create=True)
model.add(triple) -> bool
model.add(triple, graph=...) -> bool
model.remove(...) / contains(...) / clear() / clear_graph(...)
model.sync()
len(model) / model.is_empty()
model.find(*, subject=None, predicate=None, object=None, graph=None) # iterator of Quad
with model.transaction() as txn:
txn.add(...)
```
Transactions buffer operations and apply them inside one Rust
`Model::transaction` on successful exit (discard on exception). This preserves
Oxigraph/Fjall atomicity without exposing Rust callback lifetimes.
### I/O
```python
Syntax.from_name("turtle") / from_media_type / from_extension
parse(data, syntax, *, base_iri=None) -> Iterator[Quad] # str|bytes
parse_path(path, syntax=None, *, base_iri=None) -> Iterator[Quad]
load(model, data, syntax, *, collecting=True, base_iri=None) -> int
load_path(model, path, ...) -> int
serialize(model, syntax, *, base_iri=None) -> str
serialize_path(model, path, syntax, ...) -> None
```
### SPARQL
```python
query(model, sparql, *, base_iri=None, limit=None, offset=None)
# -> bool | SolutionsIter | TriplesIter
update(model, sparql, *, base_iri=None) -> None
serialize_results(model, sparql, format) -> str # ASK/SELECT via format name
```
`SolutionsIter` / `TriplesIter` hold the `Model` alive for the iterator lifetime
(store handle is shared via `Model` clone / Arc semantics).
### Utilities (curated)
`digest_hex(algorithm, data)`, `digest_bytes`, `Namespace(prefix, base)`,
`vocab` submodule IRI string constants used by examples.
## Intentional non-mirrors
| `Query` / `Update` / `Parser` / `Serializer` builders | kwargs on functions |
| `CancellationToken` | omitted |
| `Model::store` / `oxiland::sparql` | omitted |
| `World` log handlers / `tracing` | omitted (logging stays process-level) |
| Full `utility` URI/Unicode matrix | curated digest + vocab; URI helpers may ship if cheap |
| rdflib types | deferred (ADR-017) |
## Streaming / ownership
- `Model.find` already yields owned `Quad` from a `'static` store snapshot in
Rust — wrap directly.
- Parse streams use `Parser::parse_reader` over an owned `Cursor` / file so the
iterator owns its reader.
- SPARQL solution/triple iterators box a cloned `Model` before execute so the
Oxigraph iterator borrow target stays put for the Python object lifetime;
dropping the iterator releases both.
## Versioning
Python package version **0.7.0** aligns with the roadmap train. Prefer bumping
the Rust crate to 0.7.0 in the same release when publishing so docs and badges
agree; no intentional Rust public-API break for 0.7.
## Packaging
0.7.0 publishes **wheels only** to PyPI. CI builds and install-smokes each
supported OS/interpreter artifact; the ordered release workflow downloads that
exact wheel set and uploads it through OIDC Trusted Publishing. An sdist that
can build from source without a checked-out monorepo is deferred (the extension
path-depends on the parent Rust crate).