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.
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .coverage
7
+ htmlcov/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .eggs/
12
+ .venv/
13
+ venv/
14
+ .env
15
+ .idea/
16
+ .vscode/
17
+ *.swp
18
+ .DS_Store
@@ -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
+ [![CI](https://github.com/eddiethedean/triplemodel/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/triplemodel/actions/workflows/ci.yml)
37
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/eddiethedean/triplemodel)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](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
+ [![CI](https://github.com/eddiethedean/triplemodel/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/triplemodel/actions/workflows/ci.yml)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/eddiethedean/triplemodel)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](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).