Metadata-Version: 2.4
Name: sparqlmodel
Version: 0.10.0
Summary: SPARQL ORM for Python — sessions, queries, and graph persistence on RDF stores
Project-URL: Homepage, https://github.com/eddiethedean/sparqlmodel
Project-URL: Repository, https://github.com/eddiethedean/sparqlmodel
Project-URL: Documentation, https://sparqlmodel.readthedocs.io/en/latest/
Project-URL: Changelog, https://github.com/eddiethedean/sparqlmodel/blob/main/CHANGELOG.md
Author: SparqlModel Contributors
License-Expression: MIT
License-File: LICENSE
Keywords: knowledge-graph,orm,pydantic,rdf,session,sparql,sparql-orm
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pydantic<3,>=2.5
Requires-Dist: pyoxigraph<0.6,>=0.5
Requires-Dist: triplemodel<2,>=0.10.0
Requires-Dist: typing-extensions>=4.8
Provides-Extra: dev
Requires-Dist: fastapi>=0.100; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: ty>=0.0.37; extra == 'dev'
Provides-Extra: docs
Requires-Dist: fastapi>=0.100; extra == 'docs'
Requires-Dist: httpx>=0.27; extra == 'docs'
Requires-Dist: myst-parser<5,>=3.0; extra == 'docs'
Requires-Dist: sphinx-copybutton<1,>=0.5; extra == 'docs'
Requires-Dist: sphinx-design<1,>=0.6; extra == 'docs'
Requires-Dist: sphinx-rtd-theme<4,>=2.0; extra == 'docs'
Requires-Dist: sphinx<9,>=7.0; extra == 'docs'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
Requires-Dist: httpx>=0.27; extra == 'fastapi'
Provides-Extra: http
Requires-Dist: httpx>=0.27; extra == 'http'
Description-Content-Type: text/markdown

# SPARQLModel

[![PyPI version](https://img.shields.io/pypi/v/sparqlmodel.svg)](https://pypi.org/project/sparqlmodel/)
[![Python](https://img.shields.io/pypi/pyversions/sparqlmodel.svg)](https://pypi.org/project/sparqlmodel/)
[![Documentation](https://readthedocs.org/projects/sparqlmodel/badge/?version=latest)](https://sparqlmodel.readthedocs.io/en/latest/?badge=latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/eddiethedean/sparqlmodel/blob/main/LICENSE)

**The SQLModel of SPARQL** — **Pydantic v2** entity models mapped to RDF, a persistent session, and Python filters that compile to SPARQL.

Build knowledge-graph and metadata apps with typed `SPARQLModel` classes, `with SPARQLSession() as session:`, and ORM-style `put`, `get`, nested relationships, and a query builder — on in-memory graphs or remote SPARQL 1.1 endpoints. Same validation ergonomics as FastAPI and SQLModel: invalid data fails at construction and on load, before bad triples reach the store.

**Requires Python 3.10+** · Built on [TripleModel](https://github.com/eddiethedean/triplemodel) 0.10 + **pyoxigraph** · [Changelog](https://github.com/eddiethedean/sparqlmodel/blob/main/CHANGELOG.md#0100---2026-05-22) (0.10.0)

---

## Features

| Area | What you get |
|------|----------------|
| **Models** | `SPARQLModel`, `Field`, `Relationship`, `IRI` — **Pydantic v2** validation (`model_validate`, constraints, `extra="forbid"`) |
| **RDF mapping** | `rdf_type`, compact predicates, TripleModel `sync_to_graph` / `from_graph` under the hood |
| **Session** | `add`, `put`, `delete`, `get`, identity map, `flush` / pending queue (sync and **async** since 0.6) |
| **Queries** | `session.query(Person).where(Person.name == "x")` → SPARQL (`&`, `\|`, `in_`, comparisons, multi-hop) |
| **Stores** | `MemoryStore` / `AsyncMemoryStore`; `HttpStore` / `AsyncHttpStore` for Fuseki/Jena (`[http]`) |
| **FastAPI** | `SessionDep` or `AsyncSessionDep`, lifespan helpers, Turtle/JSON-LD responses |
| **Cascade** | Composition on `put`/`delete`; `Relationship(..., cascade=False)` for references |

---

## Install

```bash
pip install sparqlmodel
```

```bash
pip install "sparqlmodel[http]"      # HttpStore + AsyncHttpStore (httpx)
pip install "sparqlmodel[fastapi]"   # FastAPI session + RDF responses
pip install -e ".[dev,http,fastapi]" # development (includes pytest-asyncio for async tests)
```

For local development with [uv](https://docs.astral.sh/uv/), sync dev extras so async tests run: `uv sync --extra dev`.

---

## Quickstart

```python
from sparqlmodel import Field, IRI, Relationship, SPARQLModel, SPARQLSession

class Organization(SPARQLModel):
    rdf_type = "schema:Organization"
    __prefixes__ = {"schema": "https://schema.org/"}

    id: IRI
    name: str = Field("schema:name")

class Person(SPARQLModel):
    rdf_type = "schema:Person"
    __prefixes__ = {"schema": "https://schema.org/"}

    id: IRI
    name: str = Field("schema:name")
    works_for: Organization | None = Relationship(
        "schema:worksFor", model=Organization
    )

acme = Organization(id=IRI("urn:org:acme"), name="Acme Corp")
odos = Person(id=IRI("urn:person:odos"), name="Odos", works_for=acme)

with SPARQLSession() as session:
    session.put(odos)

    found = session.query(Person).where(Person.name == "Odos").first()
    team = session.query(Person).where(Person.works_for.name == "Acme Corp").all()
    full = session.get(Person, odos.id, depth=1)
```

---

## Pydantic models

`SPARQLModel` subclasses [`pydantic.BaseModel`](https://docs.pydantic.dev/latest/). You get the same advantages as in FastAPI or SQLModel: typed fields, IDE support, and validation on create and on load.

| When | What runs |
|------|-----------|
| `Person(...)` / API body | Pydantic validates types and `Field` constraints |
| `session.put(model)` | Validated instance → `sync_to_graph` (0.4+: same `SPARQLModel` instance subclasses `TripleModel`) |
| `session.get` / query hydration | Graph → `model_validate` → `SPARQLModel` instance |

```python
# Field forwards pydantic.Field kwargs (min_length, ge, description, …)
class Person(SPARQLModel):
    rdf_type = "schema:Person"
    __prefixes__ = {"schema": "https://schema.org/"}
    id: IRI
    name: str = Field("schema:name", min_length=1)
```

- **`extra="forbid"`** — unknown fields on a model raise at validation time (safer for APIs).
- **FastAPI** — reuse the same `SPARQLModel` classes for request/response bodies (see [FastAPI](#fastapi) below).
- **JSON-LD** — `model_dump_jsonld()` / `model_validate_jsonld()` for API dicts (cascade-aware); files and HTTP bodies use `model.serialize(format="json-ld")` or `Person.parse(...)`.

Details: [Models guide](https://sparqlmodel.readthedocs.io/en/latest/guides/models.html) · [ORM guide](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ORM.md#pydantic-integration)

---

## Session

`SPARQLSession` is the unit of work. Use it as a context manager: flush pending writes on success, roll back the pending queue on error, close HTTP stores when done.

| Method | Purpose |
|--------|---------|
| `add(model)` | Append triples (no delete of existing subject data) |
| `put(model)` | Upsert with cascade and orphan cleanup |
| `delete(model)` | Remove owned triples for root + composition tree |
| `get(Model, iri, depth=0)` | Load one resource; `depth` 0–2 eager-loads relationships |
| `query(Model).where(...)` | Fluent query; filters compile to SPARQL |
| `execute(sparql)` | Raw SPARQL SELECT (auto-prefixes when configured) |
| `flush()` / `rollback_pending()` | Apply or discard `put(..., flush=False)` queue |
| `expire(Model, iri)` | Evict identity map and hydration cache |

Nested `SPARQLModel` values are **composition** (cascade on `put`/`delete`). Use `Relationship(..., cascade=False)` or an `IRI` when the target is owned elsewhere.

---

## Query DSL

```python
with SPARQLSession() as session:
    session.query(Person).where(Person.name == "Odos").all()

    session.query(Person).where(
        (Person.name == "Odos") | (Person.name == "Ada")
    ).all()

    session.query(Person).where(
        Person.works_for.located_in.name == "Boston"
    ).all(depth=2)

    session.query(Person).where(Person.name.in_(("Odos", "Ada"))).all()

    session.query(Person).where(Person.name != "Other").all()
    # pre-0.5.2 inequality (excludes unbound): .use_inequality_for_ne()
```

Operators: `==`, `!=`, `&`, `|`, `<`, `>`, `<=`, `>=`, `.in_(tuple)` or `.in_(list)` (not a bare string — use `("x",)` for one value), multi-hop paths (`Person.works_for.name`), `.limit(n)`, `.offset(n)`, `.order_by(field, desc=False)`, `.count()`, `.is_(None)` / `.is_not(None)` on nullable relationships, `.first()` always `LIMIT 1` (ignores prior `.limit()` and `.offset()`), `.use_inequality_for_ne()`, `.use_optional_for_comparisons()` (NE semantics toggle; real `OPTIONAL` blocks are automatic on nullable hops).

---

## Stores

**MemoryStore** (default) — in-memory `triplemodel.Store` (pyoxigraph); tests and single-process apps:

```python
with SPARQLSession() as session:
    session.put(model)
```

**HttpStore** — SPARQL 1.1 over HTTP with a local mirror for `get` and cascade (`sparqlmodel[http]`):

```python
from sparqlmodel import HttpStore, SPARQLSession

with SPARQLSession(store=HttpStore("http://localhost:3030/ds/sparql")) as session:
    session.put(odos)
```

`query` / `execute` use the remote endpoint; `get` and cascade read the mirror updated by this store’s writes. See the [production guide](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/PRODUCTION.md) for mirror semantics and deployment notes.

---

## FastAPI

Per-request sessions with a shared store — same pattern as SQLModel + SQLAlchemy:

```python
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Request
from sparqlmodel import IRI
from sparqlmodel.fastapi import SessionDep, http_store_lifespan, negotiated_response

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with http_store_lifespan(app, "http://localhost:3030/ds/sparql"):
        yield

app = FastAPI(lifespan=lifespan)

@app.get("/person/{iri}")
def person(iri: str, request: Request, session: SessionDep) -> object:
    model = session.get(Person, IRI(iri))
    if model is None:
        raise HTTPException(status_code=404)
    return negotiated_response(request, model)
```

---

## Export

```python
print(odos.serialize(format="turtle"))

# or backward-compatible wrappers:
from sparqlmodel.serializers import export_model
print(export_model(odos, format="turtle"))
```

File parse/serialize is implemented by [TripleModel](https://github.com/eddiethedean/triplemodel) (`parse`, `serialize`, `load_graph`). See the [roadmap](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ROADMAP.md#forward-roadmap-07--015).

---

## Documentation

| Guide | Description |
|-------|-------------|
| **[Read the Docs](https://sparqlmodel.readthedocs.io/en/latest/)** | Full site: install, guides, API reference, troubleshooting |
| [Getting started](https://sparqlmodel.readthedocs.io/en/latest/getting-started.html) | Quickstart and first session |
| [Guides](https://sparqlmodel.readthedocs.io/en/latest/guides/index.html) | Models (Pydantic), sessions, queries, FastAPI |
| [Real-world examples](https://sparqlmodel.readthedocs.io/en/latest/guides/realworld.html) | Nobel, DCAT, Wikidata, Schema.org (`examples/realworld/`) |
| [ORM guide](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ORM.md) | Lifecycle, cascade, hydration, when to use SparqlModel vs TripleModel |
| [Technical specification](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/SPECS.md) | Normative API; [production checklist](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/SPECS.md#production-orm-checklist-13-ga-gate) |
| [Production guide](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/PRODUCTION.md) | HttpStore, sessions, deployment |
| [Roadmap](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ROADMAP.md) | 0.5–1.3 milestones; [SQLModel parity](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ROADMAP.md#sqlmodel-parity-checklist) |
| [Project plan](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/PLAN.md) | Vision and release strategy |
| [Ecosystem](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ECOSYSTEM.md) | SparqlModel vs TripleModel boundaries |

---

## Known limitations (0.10.0)

- Multi-valued predicates: first value per predicate on load; prefer `put` over `add` for upserts
- `HttpStore` / `AsyncHttpStore`: default `mirror_mode="writer"` pulls only when a subject is missing from the mirror; use `mirror_mode="remote_authoritative"` (0.10+) or `pull_subjects_into_mirror` when reads must match remote updates. Replace-on-pull (0.10+) clears stale predicates per IRI on pull. GSP full-graph sync and HTTP retries remain under [Production HttpStore](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/ROADMAP.md#production-httpstore) (**0.11–0.12**)
- Use `merge` / `refresh` / `expunge` for explicit identity-map control ([sessions guide](https://github.com/eddiethedean/sparqlmodel/blob/main/docs/guides/sessions.md#cache-control-09))
- `session.graph` is a `triplemodel.Store` (pyoxigraph), not an rdflib `Graph` — use TripleModel I/O for file round-trip
- Default `!=` uses NOT EXISTS (includes resources with no value); `.use_inequality_for_ne()` on nullable hops also treats missing links as matching
- `==`, `<`, `>`, and `in_` on optional paths still exclude unbound values (SPARQL-native)
- Nullable relationship filters use `OPTIONAL` hops; required (non-nullable) hops still use inner-join semantics
- Sessions are not thread-safe; one session per request/task
- Each model field must map to a unique RDF predicate; duplicate predicates raise `ConfigurationError` at class definition
- Cyclic embedded models raise `ConfigurationError` on `put` / `model_to_graph`
- Shared embedded resources referenced from multiple roots are preserved on `put` when another subject still links to them

---

## License

MIT — see [LICENSE](https://github.com/eddiethedean/sparqlmodel/blob/main/LICENSE).
