triplemodel 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- triplemodel-0.1.0/.gitignore +18 -0
- triplemodel-0.1.0/CHANGELOG.md +59 -0
- triplemodel-0.1.0/LICENSE +21 -0
- triplemodel-0.1.0/PKG-INFO +294 -0
- triplemodel-0.1.0/README.md +261 -0
- triplemodel-0.1.0/RELEASING.md +41 -0
- triplemodel-0.1.0/docs/ECOSYSTEM.md +203 -0
- triplemodel-0.1.0/docs/ECOSYSTEM_SPARQLMODEL.md +85 -0
- triplemodel-0.1.0/docs/PLAN.md +173 -0
- triplemodel-0.1.0/docs/ROADMAP.md +362 -0
- triplemodel-0.1.0/examples/readme_examples.py +104 -0
- triplemodel-0.1.0/pyproject.toml +79 -0
- triplemodel-0.1.0/src/triplemodel/__init__.py +46 -0
- triplemodel-0.1.0/src/triplemodel/_config.py +72 -0
- triplemodel-0.1.0/src/triplemodel/_fields.py +60 -0
- triplemodel-0.1.0/src/triplemodel/_graph.py +191 -0
- triplemodel-0.1.0/src/triplemodel/_types.py +68 -0
- triplemodel-0.1.0/src/triplemodel/model.py +102 -0
- triplemodel-0.1.0/src/triplemodel/py.typed +0 -0
- triplemodel-0.1.0/tests/test_config.py +84 -0
- triplemodel-0.1.0/tests/test_fields.py +52 -0
- triplemodel-0.1.0/tests/test_graph.py +115 -0
- triplemodel-0.1.0/tests/test_model.py +387 -0
- triplemodel-0.1.0/tests/test_packaging.py +36 -0
- triplemodel-0.1.0/tests/test_readme_examples.py +116 -0
- triplemodel-0.1.0/tests/test_types.py +122 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-05-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `TripleModel` base class with `to_graph()`, `from_graph()`, `all_from_graph()`, and `subject_uri()`
|
|
15
|
+
- `rdf_field()` / `Predicate` for mapping Pydantic fields to RDF predicates
|
|
16
|
+
- Nested `Rdf` config (`namespace`, `type_uri`, `id_field`)
|
|
17
|
+
- Low-level helpers: `model_to_graph`, `model_to_triples`, `graph_to_model`, `graph_to_models`, `models_to_graph`
|
|
18
|
+
- Public subject-IRI helpers: `subject_base()`, `id_from_subject_uri()`
|
|
19
|
+
- XSD scalar round-trip (`str`, `int`, `float`, `bool`, `date`, `datetime`)
|
|
20
|
+
- Package constants: `RDF`, `RDFS`, `XSD`, `RDF_TYPE`
|
|
21
|
+
- `py.typed` PEP 561 marker for type checkers (verified in wheel via CI build)
|
|
22
|
+
- `from_graph` / `all_from_graph` / `graph_to_model` options: `validate_type` (default `True`), `on_duplicate` (`"warn"` | `"ignore"` | `"error"`)
|
|
23
|
+
- Export `OnDuplicate` from the package root for type checkers
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `get_rdf_config` walks the class MRO so subclasses inherit a parent’s nested `Rdf` config
|
|
28
|
+
- `from_graph` checks `rdf:type` against `Rdf.type_uri` when set (pass `validate_type=False` to skip)
|
|
29
|
+
- Pydantic validation failures on import are raised as `ValueError` with model class and subject URI context
|
|
30
|
+
- Duplicate predicate objects on import emit a warning by default (first value still used until 0.2 multi-value support)
|
|
31
|
+
- Safe subject-id extraction (no false matches when one namespace is a prefix of another)
|
|
32
|
+
- Percent-encoding of id segments on subject IRI export; decode on import
|
|
33
|
+
- Plain string literals use `xsd:string`
|
|
34
|
+
- XSD booleans via `Literal.toPython()` when datatype is `xsd:boolean`
|
|
35
|
+
- `BNode` values rejected when importing into `str` fields
|
|
36
|
+
- Import errors include field, predicate, and subject context
|
|
37
|
+
- `_unwrap_optional` peels `Annotated[...]` so `Annotated[int, Predicate(...)]` imports with correct XSD coercion
|
|
38
|
+
- IRI-like `str` values use any RFC 3986 scheme (`mailto:`, `file:`, etc.) on export, not only `http`/`https`/`urn`
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- `TripleModel` uses `str_strip_whitespace=False` so RDF string values are not altered on validation
|
|
43
|
+
|
|
44
|
+
### Documentation
|
|
45
|
+
|
|
46
|
+
- README with API overview, runnable examples, and development instructions
|
|
47
|
+
- Planning docs: `docs/PLAN.md`, `docs/ROADMAP.md`, `docs/ECOSYSTEM.md`
|
|
48
|
+
- `examples/readme_examples.py` and CI tests for README snippets
|
|
49
|
+
- README limitations: `uri=` namespace alignment, empty child `Rdf`, falsy `type_uri`, `id_from_subject_uri`, BNode subjects, loose bool coercion
|
|
50
|
+
- `TripleModel` docstring: subclass `Rdf` replaces parent config (do not use empty child `class Rdf:`)
|
|
51
|
+
- CI: Python 3.11, `ruff format --check`, `python -m build`, release workflow runs `pytest` before build
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- **Prior names (pre-release):** `rdfmodel` / `tripletyped` on PyPI and GitHub; public name is **`triplemodel`** / **`TripleModel`**.
|
|
56
|
+
- **Alpha:** API may change until 1.0. Multi-value fields, nested models, sync/remove, and file I/O are planned for **0.2+**.
|
|
57
|
+
- **[SparqlModel](https://github.com/eddiethedean/sqarqlmodel)** integration (optional `triplemodel` dependency) is targeted from **0.2**; see `docs/ECOSYSTEM.md`.
|
|
58
|
+
|
|
59
|
+
[0.1.0]: https://github.com/eddiethedean/triplemodel/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TripleModel contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: triplemodel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pydantic TripleModel classes ↔ RDF triples via rdflib (alpha). SparqlModel ORM integration planned from 0.2.
|
|
5
|
+
Project-URL: Homepage, https://github.com/eddiethedean/triplemodel
|
|
6
|
+
Project-URL: Documentation, https://github.com/eddiethedean/triplemodel#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/eddiethedean/triplemodel
|
|
8
|
+
Project-URL: Changelog, https://github.com/eddiethedean/triplemodel/blob/main/CHANGELOG.md
|
|
9
|
+
Author: TripleModel contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: linked-data,pydantic,rdf,rdflib,semantic-web
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
25
|
+
Requires-Dist: rdflib<8,>=7.0
|
|
26
|
+
Requires-Dist: typing-extensions>=4.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ty>=0.0.1; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# TripleModel
|
|
35
|
+
|
|
36
|
+
[](https://github.com/eddiethedean/triplemodel/actions/workflows/ci.yml)
|
|
37
|
+
[](https://github.com/eddiethedean/triplemodel)
|
|
38
|
+
[](https://github.com/eddiethedean/triplemodel/blob/main/LICENSE)
|
|
39
|
+
|
|
40
|
+
**Pydantic models for RDF graphs.** Map typed Python classes to [rdflib](https://github.com/RDFLib/rdflib) triples and back — without hand-writing `graph.add` for every field.
|
|
41
|
+
|
|
42
|
+
| | |
|
|
43
|
+
|--|--|
|
|
44
|
+
| PyPI / import | `triplemodel` |
|
|
45
|
+
| Base class | `TripleModel` |
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
Person(slug="alice", name="Alice") → (ex:alice, foaf:name, "Alice") → Person(...)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**TripleModel** is the **typed mapping layer** in a small ecosystem: Pydantic models ↔ RDF triples via field types and predicates. [SparqlModel](https://github.com/eddiethedean/sqarqlmodel) (session, SPARQL queries, ORM) is planned to depend on TripleModel from **0.2** — see the [ecosystem guide](https://github.com/eddiethedean/triplemodel/blob/main/docs/ECOSYSTEM.md).
|
|
52
|
+
|
|
53
|
+
> **0.1.0 is alpha.** The API may change until 1.0. See [CHANGELOG](https://github.com/eddiethedean/triplemodel/blob/main/CHANGELOG.md) and the [roadmap](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Pydantic v2** models with `validate_assignment=True`
|
|
58
|
+
- **Declarative mapping** — nested `Rdf` config + `rdf_field()` or `Annotated[..., Predicate(...)]`
|
|
59
|
+
- **Subject IRIs** — build from `namespace` + `id_field`, percent-encoded segments, safe import (no prefix collisions)
|
|
60
|
+
- **XSD round-trip** — `str`, `int`, `float`, `bool`, `date`, `datetime`; IRI-like strings → `URIRef`
|
|
61
|
+
- **Stateless I/O** — `to_graph` / `from_graph` / `all_from_graph` / `models_to_graph` on in-memory `Graph`
|
|
62
|
+
- **Typed package** — `py.typed` for type checkers
|
|
63
|
+
|
|
64
|
+
**Not in 0.1.0** (on the [roadmap](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md)): file parse/serialize, multi-valued fields, nested models, sync/remove, SPARQL helpers.
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Python **3.10+**
|
|
69
|
+
- [Pydantic](https://docs.pydantic.dev/) v2
|
|
70
|
+
- [rdflib](https://rdflib.readthedocs.io/) v7
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install triplemodel
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from triplemodel import TripleModel, rdf_field
|
|
82
|
+
|
|
83
|
+
FOAF = "http://xmlns.com/foaf/0.1/"
|
|
84
|
+
|
|
85
|
+
class Person(TripleModel):
|
|
86
|
+
class Rdf:
|
|
87
|
+
namespace = "http://example.org/people/"
|
|
88
|
+
type_uri = f"{FOAF}Person"
|
|
89
|
+
id_field = "slug"
|
|
90
|
+
|
|
91
|
+
slug: str
|
|
92
|
+
name: str = rdf_field(f"{FOAF}name")
|
|
93
|
+
age: int | None = rdf_field(f"{FOAF}age", default=None)
|
|
94
|
+
|
|
95
|
+
alice = Person(slug="alice", name="Alice", age=30)
|
|
96
|
+
|
|
97
|
+
graph = alice.to_graph()
|
|
98
|
+
print(alice.subject_uri()) # http://example.org/people/alice
|
|
99
|
+
|
|
100
|
+
assert Person.from_graph(graph, alice.subject_uri()) == alice
|
|
101
|
+
assert len(Person.all_from_graph(graph)) == 1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Concepts
|
|
105
|
+
|
|
106
|
+
### RDF metadata (`class Rdf`)
|
|
107
|
+
|
|
108
|
+
| Attribute | Role |
|
|
109
|
+
|-----------|------|
|
|
110
|
+
| `namespace` | Base IRI for subject resources |
|
|
111
|
+
| `type_uri` | Emitted as `rdf:type`; used to filter `all_from_graph()` |
|
|
112
|
+
| `id_field` | Field value appended to `namespace` for the subject IRI |
|
|
113
|
+
|
|
114
|
+
Subject IRIs use `subject_base(namespace)` + percent-encoded id (`quote` / `unquote`). Override the subject IRI per call with `uri=` (round-trip works when the URI still matches `namespace`):
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
alice = Person(slug="alice", name="Alice")
|
|
118
|
+
custom_uri = "http://example.org/people/alice"
|
|
119
|
+
graph = alice.to_graph(uri=custom_uri)
|
|
120
|
+
assert Person.from_graph(graph, custom_uri) == alice
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Shared helpers (also on the package root):
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from triplemodel import id_from_subject_uri, subject_base
|
|
127
|
+
|
|
128
|
+
base = subject_base("http://example.org/people") # ensures trailing / or #
|
|
129
|
+
id_from_subject_uri("http://example.org/people", "http://example.org/people/alice") # "alice"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Field → predicate
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
name: str = rdf_field("http://xmlns.com/foaf/0.1/name")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Or with **`Annotated`**:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from typing import Annotated
|
|
142
|
+
from triplemodel import Predicate
|
|
143
|
+
|
|
144
|
+
title: Annotated[str, Predicate("http://purl.org/dc/terms/title")]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Fields **without** a predicate mapping are skipped on export and import (handy for computed or app-only fields).
|
|
148
|
+
|
|
149
|
+
Subclasses **inherit** a parent’s nested `Rdf` class when the child does not define `Rdf`. If the child declares `class Rdf:`, it **replaces** the parent’s config entirely — do not use an empty nested `Rdf` on a subclass.
|
|
150
|
+
|
|
151
|
+
### Term conversion
|
|
152
|
+
|
|
153
|
+
| Python | RDF (export) |
|
|
154
|
+
|--------|----------------|
|
|
155
|
+
| `str` (not IRI-like) | `xsd:string` literal |
|
|
156
|
+
| `str` with an RFC 3986 scheme (`http:`, `https:`, `urn:`, `mailto:`, `file:`, …) | `URIRef` |
|
|
157
|
+
| `int`, `float`, `bool`, `date`, `datetime` | XSD-typed literal |
|
|
158
|
+
|
|
159
|
+
Import uses each field’s type annotation. `BNode` objects cannot be coerced into `str` fields.
|
|
160
|
+
|
|
161
|
+
## API reference
|
|
162
|
+
|
|
163
|
+
### `TripleModel` methods
|
|
164
|
+
|
|
165
|
+
| | Method | Description |
|
|
166
|
+
|---|--------|-------------|
|
|
167
|
+
| Instance | `subject_uri(uri=None)` | Subject IRI |
|
|
168
|
+
| Instance | `to_triples(uri=None)` | `(subject, predicate, object)` tuples |
|
|
169
|
+
| Instance | `to_graph(graph=None, uri=None)` | Serialize into a `Graph` |
|
|
170
|
+
| Class | `from_graph(graph, uri, validate_type=True, on_duplicate="warn")` | Load one resource |
|
|
171
|
+
| Class | `all_from_graph(graph, type_uri=None, validate_type=True, on_duplicate="warn")` | Load all resources of this `type_uri` |
|
|
172
|
+
| Class | `rdf_config()` | Resolved `RdfConfig` |
|
|
173
|
+
|
|
174
|
+
### Module-level API
|
|
175
|
+
|
|
176
|
+
| Name | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| `rdf_field`, `Predicate`, `OnDuplicate` | Predicate metadata; duplicate-import policy type |
|
|
179
|
+
| `RdfConfig`, `TripleModel` | Config dataclass and base model |
|
|
180
|
+
| `model_to_graph`, `model_to_triples`, `models_to_graph` | Export without subclassing |
|
|
181
|
+
| `graph_to_model`, `graph_to_models` | Import into a model class |
|
|
182
|
+
| `subject_base`, `id_from_subject_uri` | Subject IRI building and parsing |
|
|
183
|
+
| `RDF`, `RDFS`, `XSD`, `RDF_TYPE` | Common namespace IRIs |
|
|
184
|
+
|
|
185
|
+
## Examples
|
|
186
|
+
|
|
187
|
+
### Batch export into one graph
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from rdflib import Graph
|
|
191
|
+
from triplemodel import TripleModel, models_to_graph, rdf_field
|
|
192
|
+
|
|
193
|
+
FOAF = "http://xmlns.com/foaf/0.1/"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Person(TripleModel):
|
|
197
|
+
class Rdf:
|
|
198
|
+
namespace = "http://example.org/people/"
|
|
199
|
+
type_uri = f"{FOAF}Person"
|
|
200
|
+
id_field = "slug"
|
|
201
|
+
|
|
202
|
+
slug: str
|
|
203
|
+
name: str = rdf_field(f"{FOAF}name")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
people = [
|
|
207
|
+
Person(slug="alice", name="Alice"),
|
|
208
|
+
Person(slug="bob", name="Bob"),
|
|
209
|
+
]
|
|
210
|
+
graph = models_to_graph(people)
|
|
211
|
+
|
|
212
|
+
# Or merge into an existing graph (rdflib Graph() is falsy when empty — pass explicitly)
|
|
213
|
+
existing = Graph()
|
|
214
|
+
models_to_graph(people, existing)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Encoded subject ids
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from triplemodel import TripleModel, rdf_field
|
|
221
|
+
|
|
222
|
+
FOAF = "http://xmlns.com/foaf/0.1/"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class Person(TripleModel):
|
|
226
|
+
class Rdf:
|
|
227
|
+
namespace = "http://example.org/people/"
|
|
228
|
+
type_uri = f"{FOAF}Person"
|
|
229
|
+
id_field = "slug"
|
|
230
|
+
|
|
231
|
+
slug: str
|
|
232
|
+
name: str = rdf_field(f"{FOAF}name")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
bob = Person(slug="bob jones", name="Bob")
|
|
236
|
+
uri = bob.subject_uri() # .../bob%20jones
|
|
237
|
+
restored = Person.from_graph(bob.to_graph(), uri)
|
|
238
|
+
assert restored == bob
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## TripleModel vs SparqlModel
|
|
242
|
+
|
|
243
|
+
| Need | Use |
|
|
244
|
+
|------|-----|
|
|
245
|
+
| Turn a model instance into triples / load from a `Graph` | **TripleModel** (`pip install triplemodel`) |
|
|
246
|
+
| Turtle/JSON-LD files, namespaces, datasets (roadmap) | **TripleModel** |
|
|
247
|
+
| `session.put`, queries, cascade delete, HTTP store | **[SparqlModel](https://github.com/eddiethedean/sqarqlmodel)** |
|
|
248
|
+
|
|
249
|
+
Details: [project plan](https://github.com/eddiethedean/triplemodel/blob/main/docs/PLAN.md) · [ecosystem guide](https://github.com/eddiethedean/triplemodel/blob/main/docs/ECOSYSTEM.md).
|
|
250
|
+
|
|
251
|
+
## Limitations (0.1.x)
|
|
252
|
+
|
|
253
|
+
- **Single value per predicate** — multiple objects import only the first; a warning is emitted by default (`on_duplicate="warn"`). Full multi-value fields land in [0.2.0](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
254
|
+
- **Flat models** — no nested `TripleModel` or RDF lists yet.
|
|
255
|
+
- **In-memory graphs only** — no `parse` / `serialize` until [0.4.0](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
256
|
+
- **No sync/remove** — re-export does not drop triples for cleared fields until [0.2.0](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
257
|
+
- **`from_graph` type check** — when `Rdf.type_uri` is set, import requires that triple unless `validate_type=False`.
|
|
258
|
+
- **`uri=` override** — `from_graph` can only derive `id_field` when the subject URI is under `Rdf.namespace`; off-namespace URIs fail validation unless you add triples another way.
|
|
259
|
+
- **Empty child `class Rdf:`** — shadows the parent and clears `namespace` / `type_uri` / `id_field`; omit `Rdf` on the child to inherit.
|
|
260
|
+
- **`type_uri=""` or other falsy config** — treated as unset (no `rdf:type` on export, no type filter on import).
|
|
261
|
+
- **`id_from_subject_uri`** — returns the URI suffix after the namespace base (may include extra `/` segments); not a single-segment validator.
|
|
262
|
+
- **`id_field` values `False` or `0`** — are valid ids (not treated as empty).
|
|
263
|
+
- **BNode subjects** — skipped in `all_from_graph()`.
|
|
264
|
+
- **Non-XSD boolean literals** — `bool` fields without `xsd:boolean` use a loose truthiness heuristic on import.
|
|
265
|
+
- **Union field types** (e.g. `str | int`) rely on rdflib `toPython()` when the annotation is not a single scalar type.
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
git clone https://github.com/eddiethedean/triplemodel.git
|
|
271
|
+
cd triplemodel
|
|
272
|
+
python -m venv .venv
|
|
273
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
274
|
+
pip install -e ".[dev]"
|
|
275
|
+
pytest
|
|
276
|
+
ruff format src tests && ruff check src tests
|
|
277
|
+
ty check src tests
|
|
278
|
+
PYTHONPATH=src python examples/readme_examples.py
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
CI runs on Python 3.10, 3.11, 3.12, and 3.13. Release steps: [RELEASING.md](https://github.com/eddiethedean/triplemodel/blob/main/RELEASING.md).
|
|
282
|
+
|
|
283
|
+
## Documentation
|
|
284
|
+
|
|
285
|
+
| Doc | Description |
|
|
286
|
+
|-----|-------------|
|
|
287
|
+
| [CHANGELOG](https://github.com/eddiethedean/triplemodel/blob/main/CHANGELOG.md) | Release notes |
|
|
288
|
+
| [Roadmap](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md) | Versions and rdflib parity |
|
|
289
|
+
| [Plan](https://github.com/eddiethedean/triplemodel/blob/main/docs/PLAN.md) | Strategy and priorities |
|
|
290
|
+
| [Ecosystem](https://github.com/eddiethedean/triplemodel/blob/main/docs/ECOSYSTEM.md) | triplemodel ↔ SparqlModel boundaries |
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT — see [LICENSE](https://github.com/eddiethedean/triplemodel/blob/main/LICENSE).
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# TripleModel
|
|
2
|
+
|
|
3
|
+
[](https://github.com/eddiethedean/triplemodel/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/eddiethedean/triplemodel)
|
|
5
|
+
[](https://github.com/eddiethedean/triplemodel/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
**Pydantic models for RDF graphs.** Map typed Python classes to [rdflib](https://github.com/RDFLib/rdflib) triples and back — without hand-writing `graph.add` for every field.
|
|
8
|
+
|
|
9
|
+
| | |
|
|
10
|
+
|--|--|
|
|
11
|
+
| PyPI / import | `triplemodel` |
|
|
12
|
+
| Base class | `TripleModel` |
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
Person(slug="alice", name="Alice") → (ex:alice, foaf:name, "Alice") → Person(...)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**TripleModel** is the **typed mapping layer** in a small ecosystem: Pydantic models ↔ RDF triples via field types and predicates. [SparqlModel](https://github.com/eddiethedean/sqarqlmodel) (session, SPARQL queries, ORM) is planned to depend on TripleModel from **0.2** — see the [ecosystem guide](https://github.com/eddiethedean/triplemodel/blob/main/docs/ECOSYSTEM.md).
|
|
19
|
+
|
|
20
|
+
> **0.1.0 is alpha.** The API may change until 1.0. See [CHANGELOG](https://github.com/eddiethedean/triplemodel/blob/main/CHANGELOG.md) and the [roadmap](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Pydantic v2** models with `validate_assignment=True`
|
|
25
|
+
- **Declarative mapping** — nested `Rdf` config + `rdf_field()` or `Annotated[..., Predicate(...)]`
|
|
26
|
+
- **Subject IRIs** — build from `namespace` + `id_field`, percent-encoded segments, safe import (no prefix collisions)
|
|
27
|
+
- **XSD round-trip** — `str`, `int`, `float`, `bool`, `date`, `datetime`; IRI-like strings → `URIRef`
|
|
28
|
+
- **Stateless I/O** — `to_graph` / `from_graph` / `all_from_graph` / `models_to_graph` on in-memory `Graph`
|
|
29
|
+
- **Typed package** — `py.typed` for type checkers
|
|
30
|
+
|
|
31
|
+
**Not in 0.1.0** (on the [roadmap](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md)): file parse/serialize, multi-valued fields, nested models, sync/remove, SPARQL helpers.
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python **3.10+**
|
|
36
|
+
- [Pydantic](https://docs.pydantic.dev/) v2
|
|
37
|
+
- [rdflib](https://rdflib.readthedocs.io/) v7
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install triplemodel
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from triplemodel import TripleModel, rdf_field
|
|
49
|
+
|
|
50
|
+
FOAF = "http://xmlns.com/foaf/0.1/"
|
|
51
|
+
|
|
52
|
+
class Person(TripleModel):
|
|
53
|
+
class Rdf:
|
|
54
|
+
namespace = "http://example.org/people/"
|
|
55
|
+
type_uri = f"{FOAF}Person"
|
|
56
|
+
id_field = "slug"
|
|
57
|
+
|
|
58
|
+
slug: str
|
|
59
|
+
name: str = rdf_field(f"{FOAF}name")
|
|
60
|
+
age: int | None = rdf_field(f"{FOAF}age", default=None)
|
|
61
|
+
|
|
62
|
+
alice = Person(slug="alice", name="Alice", age=30)
|
|
63
|
+
|
|
64
|
+
graph = alice.to_graph()
|
|
65
|
+
print(alice.subject_uri()) # http://example.org/people/alice
|
|
66
|
+
|
|
67
|
+
assert Person.from_graph(graph, alice.subject_uri()) == alice
|
|
68
|
+
assert len(Person.all_from_graph(graph)) == 1
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Concepts
|
|
72
|
+
|
|
73
|
+
### RDF metadata (`class Rdf`)
|
|
74
|
+
|
|
75
|
+
| Attribute | Role |
|
|
76
|
+
|-----------|------|
|
|
77
|
+
| `namespace` | Base IRI for subject resources |
|
|
78
|
+
| `type_uri` | Emitted as `rdf:type`; used to filter `all_from_graph()` |
|
|
79
|
+
| `id_field` | Field value appended to `namespace` for the subject IRI |
|
|
80
|
+
|
|
81
|
+
Subject IRIs use `subject_base(namespace)` + percent-encoded id (`quote` / `unquote`). Override the subject IRI per call with `uri=` (round-trip works when the URI still matches `namespace`):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
alice = Person(slug="alice", name="Alice")
|
|
85
|
+
custom_uri = "http://example.org/people/alice"
|
|
86
|
+
graph = alice.to_graph(uri=custom_uri)
|
|
87
|
+
assert Person.from_graph(graph, custom_uri) == alice
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Shared helpers (also on the package root):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from triplemodel import id_from_subject_uri, subject_base
|
|
94
|
+
|
|
95
|
+
base = subject_base("http://example.org/people") # ensures trailing / or #
|
|
96
|
+
id_from_subject_uri("http://example.org/people", "http://example.org/people/alice") # "alice"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Field → predicate
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
name: str = rdf_field("http://xmlns.com/foaf/0.1/name")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or with **`Annotated`**:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from typing import Annotated
|
|
109
|
+
from triplemodel import Predicate
|
|
110
|
+
|
|
111
|
+
title: Annotated[str, Predicate("http://purl.org/dc/terms/title")]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Fields **without** a predicate mapping are skipped on export and import (handy for computed or app-only fields).
|
|
115
|
+
|
|
116
|
+
Subclasses **inherit** a parent’s nested `Rdf` class when the child does not define `Rdf`. If the child declares `class Rdf:`, it **replaces** the parent’s config entirely — do not use an empty nested `Rdf` on a subclass.
|
|
117
|
+
|
|
118
|
+
### Term conversion
|
|
119
|
+
|
|
120
|
+
| Python | RDF (export) |
|
|
121
|
+
|--------|----------------|
|
|
122
|
+
| `str` (not IRI-like) | `xsd:string` literal |
|
|
123
|
+
| `str` with an RFC 3986 scheme (`http:`, `https:`, `urn:`, `mailto:`, `file:`, …) | `URIRef` |
|
|
124
|
+
| `int`, `float`, `bool`, `date`, `datetime` | XSD-typed literal |
|
|
125
|
+
|
|
126
|
+
Import uses each field’s type annotation. `BNode` objects cannot be coerced into `str` fields.
|
|
127
|
+
|
|
128
|
+
## API reference
|
|
129
|
+
|
|
130
|
+
### `TripleModel` methods
|
|
131
|
+
|
|
132
|
+
| | Method | Description |
|
|
133
|
+
|---|--------|-------------|
|
|
134
|
+
| Instance | `subject_uri(uri=None)` | Subject IRI |
|
|
135
|
+
| Instance | `to_triples(uri=None)` | `(subject, predicate, object)` tuples |
|
|
136
|
+
| Instance | `to_graph(graph=None, uri=None)` | Serialize into a `Graph` |
|
|
137
|
+
| Class | `from_graph(graph, uri, validate_type=True, on_duplicate="warn")` | Load one resource |
|
|
138
|
+
| Class | `all_from_graph(graph, type_uri=None, validate_type=True, on_duplicate="warn")` | Load all resources of this `type_uri` |
|
|
139
|
+
| Class | `rdf_config()` | Resolved `RdfConfig` |
|
|
140
|
+
|
|
141
|
+
### Module-level API
|
|
142
|
+
|
|
143
|
+
| Name | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `rdf_field`, `Predicate`, `OnDuplicate` | Predicate metadata; duplicate-import policy type |
|
|
146
|
+
| `RdfConfig`, `TripleModel` | Config dataclass and base model |
|
|
147
|
+
| `model_to_graph`, `model_to_triples`, `models_to_graph` | Export without subclassing |
|
|
148
|
+
| `graph_to_model`, `graph_to_models` | Import into a model class |
|
|
149
|
+
| `subject_base`, `id_from_subject_uri` | Subject IRI building and parsing |
|
|
150
|
+
| `RDF`, `RDFS`, `XSD`, `RDF_TYPE` | Common namespace IRIs |
|
|
151
|
+
|
|
152
|
+
## Examples
|
|
153
|
+
|
|
154
|
+
### Batch export into one graph
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from rdflib import Graph
|
|
158
|
+
from triplemodel import TripleModel, models_to_graph, rdf_field
|
|
159
|
+
|
|
160
|
+
FOAF = "http://xmlns.com/foaf/0.1/"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Person(TripleModel):
|
|
164
|
+
class Rdf:
|
|
165
|
+
namespace = "http://example.org/people/"
|
|
166
|
+
type_uri = f"{FOAF}Person"
|
|
167
|
+
id_field = "slug"
|
|
168
|
+
|
|
169
|
+
slug: str
|
|
170
|
+
name: str = rdf_field(f"{FOAF}name")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
people = [
|
|
174
|
+
Person(slug="alice", name="Alice"),
|
|
175
|
+
Person(slug="bob", name="Bob"),
|
|
176
|
+
]
|
|
177
|
+
graph = models_to_graph(people)
|
|
178
|
+
|
|
179
|
+
# Or merge into an existing graph (rdflib Graph() is falsy when empty — pass explicitly)
|
|
180
|
+
existing = Graph()
|
|
181
|
+
models_to_graph(people, existing)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Encoded subject ids
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from triplemodel import TripleModel, rdf_field
|
|
188
|
+
|
|
189
|
+
FOAF = "http://xmlns.com/foaf/0.1/"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Person(TripleModel):
|
|
193
|
+
class Rdf:
|
|
194
|
+
namespace = "http://example.org/people/"
|
|
195
|
+
type_uri = f"{FOAF}Person"
|
|
196
|
+
id_field = "slug"
|
|
197
|
+
|
|
198
|
+
slug: str
|
|
199
|
+
name: str = rdf_field(f"{FOAF}name")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
bob = Person(slug="bob jones", name="Bob")
|
|
203
|
+
uri = bob.subject_uri() # .../bob%20jones
|
|
204
|
+
restored = Person.from_graph(bob.to_graph(), uri)
|
|
205
|
+
assert restored == bob
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## TripleModel vs SparqlModel
|
|
209
|
+
|
|
210
|
+
| Need | Use |
|
|
211
|
+
|------|-----|
|
|
212
|
+
| Turn a model instance into triples / load from a `Graph` | **TripleModel** (`pip install triplemodel`) |
|
|
213
|
+
| Turtle/JSON-LD files, namespaces, datasets (roadmap) | **TripleModel** |
|
|
214
|
+
| `session.put`, queries, cascade delete, HTTP store | **[SparqlModel](https://github.com/eddiethedean/sqarqlmodel)** |
|
|
215
|
+
|
|
216
|
+
Details: [project plan](https://github.com/eddiethedean/triplemodel/blob/main/docs/PLAN.md) · [ecosystem guide](https://github.com/eddiethedean/triplemodel/blob/main/docs/ECOSYSTEM.md).
|
|
217
|
+
|
|
218
|
+
## Limitations (0.1.x)
|
|
219
|
+
|
|
220
|
+
- **Single value per predicate** — multiple objects import only the first; a warning is emitted by default (`on_duplicate="warn"`). Full multi-value fields land in [0.2.0](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
221
|
+
- **Flat models** — no nested `TripleModel` or RDF lists yet.
|
|
222
|
+
- **In-memory graphs only** — no `parse` / `serialize` until [0.4.0](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
223
|
+
- **No sync/remove** — re-export does not drop triples for cleared fields until [0.2.0](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md).
|
|
224
|
+
- **`from_graph` type check** — when `Rdf.type_uri` is set, import requires that triple unless `validate_type=False`.
|
|
225
|
+
- **`uri=` override** — `from_graph` can only derive `id_field` when the subject URI is under `Rdf.namespace`; off-namespace URIs fail validation unless you add triples another way.
|
|
226
|
+
- **Empty child `class Rdf:`** — shadows the parent and clears `namespace` / `type_uri` / `id_field`; omit `Rdf` on the child to inherit.
|
|
227
|
+
- **`type_uri=""` or other falsy config** — treated as unset (no `rdf:type` on export, no type filter on import).
|
|
228
|
+
- **`id_from_subject_uri`** — returns the URI suffix after the namespace base (may include extra `/` segments); not a single-segment validator.
|
|
229
|
+
- **`id_field` values `False` or `0`** — are valid ids (not treated as empty).
|
|
230
|
+
- **BNode subjects** — skipped in `all_from_graph()`.
|
|
231
|
+
- **Non-XSD boolean literals** — `bool` fields without `xsd:boolean` use a loose truthiness heuristic on import.
|
|
232
|
+
- **Union field types** (e.g. `str | int`) rely on rdflib `toPython()` when the annotation is not a single scalar type.
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
git clone https://github.com/eddiethedean/triplemodel.git
|
|
238
|
+
cd triplemodel
|
|
239
|
+
python -m venv .venv
|
|
240
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
241
|
+
pip install -e ".[dev]"
|
|
242
|
+
pytest
|
|
243
|
+
ruff format src tests && ruff check src tests
|
|
244
|
+
ty check src tests
|
|
245
|
+
PYTHONPATH=src python examples/readme_examples.py
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
CI runs on Python 3.10, 3.11, 3.12, and 3.13. Release steps: [RELEASING.md](https://github.com/eddiethedean/triplemodel/blob/main/RELEASING.md).
|
|
249
|
+
|
|
250
|
+
## Documentation
|
|
251
|
+
|
|
252
|
+
| Doc | Description |
|
|
253
|
+
|-----|-------------|
|
|
254
|
+
| [CHANGELOG](https://github.com/eddiethedean/triplemodel/blob/main/CHANGELOG.md) | Release notes |
|
|
255
|
+
| [Roadmap](https://github.com/eddiethedean/triplemodel/blob/main/docs/ROADMAP.md) | Versions and rdflib parity |
|
|
256
|
+
| [Plan](https://github.com/eddiethedean/triplemodel/blob/main/docs/PLAN.md) | Strategy and priorities |
|
|
257
|
+
| [Ecosystem](https://github.com/eddiethedean/triplemodel/blob/main/docs/ECOSYSTEM.md) | triplemodel ↔ SparqlModel boundaries |
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT — see [LICENSE](https://github.com/eddiethedean/triplemodel/blob/main/LICENSE).
|