diff --git a/glowtables/table.py b/glowtables/table.py index f133688..98533a9 100644 --- a/glowtables/table.py +++ b/glowtables/table.py @@ -23,6 +23,8 @@ from typing import Any, Callable, Generic, Iterator, NewType, Optional, TypeVar import rdflib +from glowtables.sparql import SparqlBackend + Language = NewType("Language", str) """ISO 639-1 code""" @@ -42,6 +44,9 @@ class Field(abc.ABC, Generic[_TFieldValue]): display_names: dict[Language, str] """Localized name for the field (eg. in a table header)""" + parse: Callable[[str], _TFieldValue] + """Parses a string returned by a SPARQL query to a native Python value.""" + @abc.abstractmethod def sort_key(self, value: _TFieldValue): """Function suitable as ``key`` argument to :func:`sorted`. @@ -72,18 +77,18 @@ class Field(abc.ABC, Generic[_TFieldValue]): @dataclasses.dataclass -class LiteralField(Field[rdflib.Literal]): +class LiteralField(Field[_TFieldValue], Generic[_TFieldValue]): """Simplest field: its value is a literal directly on the subject""" predicate: rdflib.URIRef - default: Optional[rdflib.Literal] = None + default: Optional[_TFieldValue] = None """If this is not :const:`None`, allows subjects without a statement for this field; and use this value instead when sorting. This is only used when sorting, and isn't displayed.""" - def sort_key(self, value: rdflib.Literal) -> Any: + def sort_key(self, value: Optional[_TFieldValue]) -> Any: """Function suitable as ``key`` argument to :func:`sorted`. Defaults to the identity function.""" @@ -176,3 +181,13 @@ class Table: constraints=constraints, statements=statements.strip(), ) + + def query(self, backend: SparqlBackend) -> Iterator[tuple]: + """Returns a list of all rows of the table. Each row has exactly one cell for + each column defined in :attr:`fields`. + """ + for row in backend.query(self.sparql()): + yield tuple( + None if cell is None else field.parse(cell) + for (field, cell) in zip(self.fields, row) + ) diff --git a/glowtables/tests/table_test.py b/glowtables/tests/table_test.py index d592150..aaadcff 100644 --- a/glowtables/tests/table_test.py +++ b/glowtables/tests/table_test.py @@ -18,6 +18,7 @@ Test cases use a graph containing a few CPUs, with names and clock frequencies. """ import textwrap +from decimal import Decimal import pytest import rdflib @@ -64,6 +65,7 @@ def test_single_literal(rdflib_sparql: SparqlBackend) -> None: name_field = LiteralField( "display_name", {Language("en"): "Name"}, + str, rdflib.URIRef("http://example.org/display-name"), ) table = Table( @@ -81,7 +83,7 @@ def test_single_literal(rdflib_sparql: SparqlBackend) -> None: """ ) - assert set(rdflib_sparql.query(table.sparql())) == { + assert set(table.query(rdflib_sparql)) == { ("Grown 1700",), ("Grown 3600",), } @@ -91,11 +93,13 @@ def test_two_literals(rdflib_sparql: SparqlBackend) -> None: name_field = LiteralField( "display_name", {Language("en"): "Name"}, + str, rdflib.URIRef("http://example.org/display-name"), ) frequency_field = LiteralField( "frequency", {Language("en"): "Clock frequency"}, + Decimal, rdflib.URIRef("http://example.org/clock-frequency"), ) table = Table( @@ -114,8 +118,8 @@ def test_two_literals(rdflib_sparql: SparqlBackend) -> None: """ ) - assert set(rdflib_sparql.query(table.sparql())) == { - ("Grown 1700", "3000"), + assert set(table.query(rdflib_sparql)) == { + ("Grown 1700", 3000), } @@ -123,14 +127,16 @@ def test_default_value(rdflib_sparql: SparqlBackend) -> None: name_field = LiteralField( "display_name", {Language("en"): "Name"}, + str, rdflib.URIRef("http://example.org/display-name"), - default=rdflib.Literal("Anonymous CPU"), + default="Anonymous CPU", ) frequency_field = LiteralField( "frequency", {Language("en"): "Clock frequency"}, + Decimal, rdflib.URIRef("http://example.org/clock-frequency"), - default=rdflib.Literal(0), + default=Decimal(0), ) table = Table( fields=[name_field, frequency_field], @@ -148,10 +154,10 @@ def test_default_value(rdflib_sparql: SparqlBackend) -> None: """ ) - assert set(rdflib_sparql.query(table.sparql())) == { - ("Grown 1700", "3000"), + assert set(table.query(rdflib_sparql)) == { + ("Grown 1700", 3000), ("Grown 3600", None), - (None, "9000"), + (None, 9000), } @@ -159,6 +165,7 @@ def test_field_id_subject() -> None: name_field = LiteralField( "subject", {Language("en"): "Name"}, + str, rdflib.URIRef("http://example.org/display-name"), ) with pytest.raises(ValueError, match="both subject and a field id"): @@ -172,11 +179,13 @@ def test_field_id_clash() -> None: name_field = LiteralField( "name", {Language("en"): "Name"}, + str, rdflib.URIRef("http://example.org/name"), ) display_name_field = LiteralField( "name", {Language("en"): "Display Name"}, + str, rdflib.URIRef("http://example.org/display-name"), ) with pytest.raises(ValueError, match="has duplicate field ids: name"):