Skip to content

Commit 94e8609

Browse files
authored
Represent triples with a Pydantic model (#172)
1 parent 4c41bd9 commit 94e8609

2 files changed

Lines changed: 34 additions & 18 deletions

File tree

src/curies/triples.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
import csv
66
import gzip
7-
from collections.abc import Generator, Iterable
7+
from collections.abc import Generator, Iterable, Sequence
88
from contextlib import contextmanager
99
from pathlib import Path
10-
from typing import NamedTuple, TextIO
10+
from typing import TextIO
1111

12+
from pydantic import BaseModel
1213
from typing_extensions import Self
1314

14-
from curies import Reference
15+
from .api import Reference
1516

1617
__all__ = [
1718
"Triple",
@@ -20,24 +21,32 @@
2021
]
2122

2223

23-
class Triple(NamedTuple):
24-
"""A three-tuple of reference, useful for semantic web applications."""
24+
class Triple(BaseModel):
25+
"""A model for a triple of subject-predicate-object triple."""
2526

2627
subject: Reference
2728
predicate: Reference
2829
object: Reference
2930

3031
@classmethod
31-
def from_curies(cls, subject_curie: str, predicate_curie: str, object_curie: str) -> Self:
32+
def from_curies(
33+
cls,
34+
subject_curie: str,
35+
predicate_curie: str,
36+
object_curie: str,
37+
*,
38+
reference_cls: type[Reference] = Reference,
39+
) -> Self:
3240
"""Construct a triple from three CURIE strings."""
3341
return cls(
34-
Reference.from_curie(subject_curie),
35-
Reference.from_curie(predicate_curie),
36-
Reference.from_curie(object_curie),
42+
subject=reference_cls.from_curie(subject_curie),
43+
predicate=reference_cls.from_curie(predicate_curie),
44+
object=reference_cls.from_curie(object_curie),
3745
)
3846

3947

40-
HEADER = Triple._fields
48+
#: the default header for a three-column file representing triples
49+
HEADER = list(Triple.model_fields)
4150

4251

4352
@contextmanager
@@ -49,11 +58,15 @@ def _get_file(path: str | Path, read: bool) -> Generator[TextIO, None, None]:
4958
yield open(path, mode="r" if read else "w")
5059

5160

52-
def write_triples(triples: Iterable[Triple], path: str | Path) -> None:
61+
def write_triples(
62+
triples: Iterable[Triple], path: str | Path, *, header: Sequence[str] | None = None
63+
) -> None:
5364
"""Write triples to a file."""
65+
if header is None:
66+
header = HEADER
5467
with _get_file(path, read=False) as file:
5568
writer = csv.writer(file, delimiter="\t")
56-
writer.writerow(HEADER)
69+
writer.writerow(header)
5770
writer.writerows(
5871
(triple.subject.curie, triple.predicate.curie, triple.object.curie)
5972
for triple in triples
@@ -69,9 +82,9 @@ def read_triples(path: str | Path, *, reference_cls: type[Reference] | None = No
6982
_header = next(reader)
7083
return [
7184
Triple(
72-
reference_cls.from_curie(subject_curie),
73-
reference_cls.from_curie(predicate_curie),
74-
reference_cls.from_curie(object_curie),
85+
subject=reference_cls.from_curie(subject_curie),
86+
predicate=reference_cls.from_curie(predicate_curie),
87+
object=reference_cls.from_curie(object_curie),
7588
)
7689
for subject_curie, predicate_curie, object_curie in reader
7790
]

tests/test_triples.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for triples."""
22

3+
import itertools as itt
34
import tempfile
45
import unittest
56
from pathlib import Path
@@ -18,11 +19,13 @@ def test_roundtrip(self) -> None:
1819
Triple.from_curies("a:1", "a:2", "a:4"),
1920
]
2021
with tempfile.TemporaryDirectory() as directory:
21-
for path in [
22+
paths = [
2223
Path(directory).joinpath("test.tsv.gz"),
2324
Path(directory).joinpath("test.tsv"),
24-
]:
25+
]
26+
headers = [None, ("a", "b", "c")]
27+
for path, header in itt.product(paths, headers):
2528
with self.subTest(path=path):
26-
write_triples(triples, path)
29+
write_triples(triples, path, header=header)
2730
reconstituted = read_triples(path)
2831
self.assertEqual(triples, reconstituted)

0 commit comments

Comments
 (0)