aboutsummaryrefslogtreecommitdiffstats
path: root/bindings
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2020-07-14 00:55:52 +0200
committerDavid Robillard <d@drobilla.net>2020-10-27 13:13:59 +0100
commit207d78fafb99d26363e6e239c653482694d32510 (patch)
tree82347a17deeb6cb3c78ab1c7e4bc3832a6627010 /bindings
parent0bca3b5c25e278e92ce9f99e79151c3a02a97ed1 (diff)
downloadserd-207d78fafb99d26363e6e239c653482694d32510.tar.gz
serd-207d78fafb99d26363e6e239c653482694d32510.tar.bz2
serd-207d78fafb99d26363e6e239c653482694d32510.zip
WIP: Generate Sphinx documentation
Diffstat (limited to 'bindings')
-rw-r--r--bindings/python/_static/custom.css31
-rw-r--r--bindings/python/_static/serd.svg135
-rw-r--r--bindings/python/conf.py91
-rw-r--r--bindings/python/index.rst11
-rw-r--r--bindings/python/overview.rst616
-rw-r--r--bindings/python/reference.rst8
6 files changed, 892 insertions, 0 deletions
diff --git a/bindings/python/_static/custom.css b/bindings/python/_static/custom.css
new file mode 100644
index 00000000..37807922
--- /dev/null
+++ b/bindings/python/_static/custom.css
@@ -0,0 +1,31 @@
+div.document {
+ margin : 0
+}
+
+div.body {
+ margin-top : 2em
+}
+
+div.sphinxsidebarwrapper {
+ background : #EEE
+}
+
+div.sphinxsidebarwrapper p.blurb {
+ text-align : center
+}
+
+img.logo {
+ width : 6em
+}
+
+.class {
+ padding-top : 1.5em
+}
+
+.function {
+ padding-top : 1.5em
+}
+
+.method {
+ padding-top : 0.75em
+}
diff --git a/bindings/python/_static/serd.svg b/bindings/python/_static/serd.svg
new file mode 100644
index 00000000..6682c2e2
--- /dev/null
+++ b/bindings/python/_static/serd.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ sodipodi:docname="serd.svg"
+ width="33.866669mm"
+ height="33.866669mm"
+ viewBox="0 0 33.866668 33.866668"
+ version="1.1"
+ id="svg8">
+ <sodipodi:namedview
+ inkscape:current-layer="svg8"
+ inkscape:window-maximized="0"
+ inkscape:window-y="48"
+ inkscape:window-x="12"
+ inkscape:cy="62.700126"
+ inkscape:cx="-7.2291926"
+ inkscape:zoom="4.2953355"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="false"
+ id="namedview26"
+ inkscape:window-height="2100"
+ inkscape:window-width="3816"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#ffffff" />
+ <defs
+ id="defs2" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <path
+ d="M 26.726105,7.1405637 H 33.25462 V 0.61204813 h -6.528515 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path889" />
+ <path
+ d="M 26.726105,7.1405637 H 33.25462 V 0.61204813 h -6.528515 z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path891" />
+ <path
+ d="m 13.669077,7.1405637 h 6.528516 V 0.61204813 h -6.528516 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path893" />
+ <path
+ d="m 13.669077,7.1405637 h 6.528516 V 0.61204813 h -6.528516 z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path895" />
+ <path
+ d="M 0.61204754,7.1405637 H 7.1405623 V 0.61204813 H 0.61204754 Z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path897" />
+ <path
+ d="M 0.61204754,7.1405637 H 7.1405623 V 0.61204813 H 0.61204754 Z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path899" />
+ <path
+ d="M 26.726105,33.254621 H 33.25462 V 26.726109 H 26.726105 Z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path901" />
+ <path
+ d="M 26.726105,33.254621 H 33.25462 V 26.726109 H 26.726105 Z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path903" />
+ <path
+ d="m 13.669077,33.254621 h 6.528516 v -6.528512 h -6.528516 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path905" />
+ <path
+ d="m 13.669077,33.254621 h 6.528516 v -6.528512 h -6.528516 z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path907" />
+ <path
+ d="M 0.61204754,33.254621 H 7.1405623 V 26.726109 H 0.61204754 Z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path909" />
+ <path
+ d="M 0.61204754,33.254621 H 7.1405623 V 26.726109 H 0.61204754 Z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path911" />
+ <path
+ d="m 13.669077,20.197594 h 6.528516 v -6.528515 h -6.528516 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#444444;stroke-width:1.05833325;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path913" />
+ <path
+ d="m 13.669077,20.197594 h 6.528516 v -6.528515 h -6.528516 z"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path915" />
+ <path
+ d="m 20.197593,3.8763056 h 6.528512"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path917" />
+ <path
+ d="M 7.1405623,3.8763056 H 13.669077"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path919" />
+ <path
+ d="M 7.1405623,7.1405637 13.669077,13.669079"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path921" />
+ <path
+ d="m 20.197593,20.197594 6.528512,6.528515"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path923" />
+ <path
+ d="M 7.1405623,29.990363 H 13.669077"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path925" />
+ <path
+ d="m 20.197593,29.990363 h 6.528512"
+ style="fill:none;stroke:#444444;stroke-width:1.05833325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path927" />
+</svg>
diff --git a/bindings/python/conf.py b/bindings/python/conf.py
new file mode 100644
index 00000000..3f9acac1
--- /dev/null
+++ b/bindings/python/conf.py
@@ -0,0 +1,91 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../../build/bindings/python'))
+
+
+
+from unittest.mock import Mock as MagicMock
+
+class Mock(MagicMock):
+ @classmethod
+ def __getattr__(cls, name):
+ return MagicMock()
+
+MOCK_MODULES = ['cython', 'libc.stdint']
+sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Serd'
+copyright = '2020, David Robillard'
+author = 'David Robillard'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.viewcode',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+
+html_theme_options = {'body_max_width': '80em'}
+
+html_sidebars = {
+ '**': [
+ 'about.html',
+ 'localtoc.html',
+ 'donate.html',
+ ]
+}
+
+html_theme_options = {
+ 'description': 'A lightweight library for working with RDF data.',
+ 'donate_url': 'http://drobilla.net/pages/donate.html',
+ 'github_repo': 'serd',
+ 'github_user': 'drobilla',
+ 'logo': 'serd.svg',
+ 'logo_name': True,
+ 'logo_text_align': 'center',
+ 'page_width': '80em - 15em',
+ 'sidebar_width': '15em',
+}
diff --git a/bindings/python/index.rst b/bindings/python/index.rst
new file mode 100644
index 00000000..d939ed49
--- /dev/null
+++ b/bindings/python/index.rst
@@ -0,0 +1,11 @@
+#########################
+Serd Python Documentation
+#########################
+
+.. toctree::
+
+ overview
+ reference
+
+:ref:`genindex`
+:ref:`modindex`
diff --git a/bindings/python/overview.rst b/bindings/python/overview.rst
new file mode 100644
index 00000000..582c3762
--- /dev/null
+++ b/bindings/python/overview.rst
@@ -0,0 +1,616 @@
+.. testsetup:: *
+
+ import serd
+
+========
+Overview
+========
+
+Serd is a lightweight C library for working with RDF data. This is the
+documentation for its Python bindings, which also serves as a gentle
+introduction to the basics of RDF.
+
+Serd is designed for high-performance or resource-constrained applications, and
+makes it possible to work with very large documents quickly and/or using
+minimal memory. In particular, it is dramatically faster than `rdflib
+<https://rdflib.readthedocs.io/en/stable/>`_, though it is less fully-featured
+and not pure Python.
+
+Nodes
+=====
+
+Nodes are the basic building blocks of data. Nodes are essentially strings:
+
+>>> print(serd.uri("http://example.org/something"))
+http://example.org/something
+
+>>> print(serd.string("hello"))
+hello
+
+>>> print(serd.decimal(1234))
+1234.0
+
+>>> len(serd.string("hello"))
+5
+
+However, nodes also have a :meth:`~serd.Node.type`, and optionally either a
+:meth:`~serd.Node.datatype` or :meth:`~serd.Node.language`.
+
+Representation
+--------------
+
+The string content of a node as shown above can be ambiguous. For example, it
+is impossible to tell a URI from a string literal using only their string
+contents. The :meth:`~serd.Node.to_syntax` method returns a complete
+representation of a node, in the `Turtle <https://www.w3.org/TR/turtle/>`_
+syntax by default:
+
+>>> print(serd.uri("http://example.org/something").to_syntax())
+<http://example.org/something>
+
+>>> print(serd.string("hello").to_syntax())
+"hello"
+
+>>> print(serd.decimal(1234).to_syntax())
+1234.0
+
+Note that the representation of a node in some syntax *may* be the same as the
+``str()`` contents which are printed, but this is usually not the case. For
+example, as shown above, URIs and strings are quoted differently in Turtle.
+
+A different syntax can be used by specifying one explicitly:
+
+>>> print(serd.decimal(1234).to_syntax(serd.Syntax.NTRIPLES))
+"1234.0"^^<http://www.w3.org/2001/XMLSchema#decimal>
+
+An identical node can be recreated from such a string using the
+:meth:`~serd.Node.from_syntax` method:
+
+>>> node = serd.decimal(1234)
+>>> copy = serd.Node.from_syntax(node.to_syntax()) # Don't actually do this
+>>> print(copy)
+1234.0
+
+Alternatively, the ``repr()`` builtin will return the Python construction
+representation:
+
+>>> repr(serd.decimal(1234))
+'serd.typed_literal("1234.0", "http://www.w3.org/2001/XMLSchema#decimal")'
+
+Any node can be round-tripped to and from a string using these methods. That
+is, for any node `n`, both::
+
+ serd.Node.from_syntax(n.to_syntax())
+
+and::
+
+ eval(repr(n))
+
+produce an equivalent node. Using the `to_syntax()` method is generally
+recommended, since it uses standard syntax.
+
+Primitives
+----------
+
+For convenience, nodes can be constructed from Python primitives by simply
+passing a value to the constructor:
+
+>>> repr(serd.Node(True))
+'serd.boolean(True)'
+>>> repr(serd.Node("hello"))
+'serd.string("hello")'
+>>> repr(serd.Node(1234))
+'serd.typed_literal("1234", "http://www.w3.org/2001/XMLSchema#integer")'
+>>> repr(serd.Node(12.34))
+'serd.typed_literal("1.234E1", "http://www.w3.org/2001/XMLSchema#double")'
+
+Note that it is not possible to construct every type of node this way, and care
+should be taken to not accidentally construct a string literal where a URI is
+desired.
+
+Fundamental Constructors
+------------------------
+
+As the above examples suggest, several node constructors are just convenience
+wrappers for more fundamental ones. All node constructors reduce to one of the
+following:
+
+ * :func:`serd.plain_literal` - A string with optional language, like
+ `"hallo"@de` in Turtle.
+
+ * :func:`serd.typed_literal` - A string with optional datatype, like
+ ``"1.2E9"^^xsd:float`` in Turtle.
+
+ * :func:`serd.blank` - A blank node, like "b42", which would be ``_:b42`` in
+ Turtle.
+
+ * :func:`serd.curie` - A compact URI, like "eg:name".
+
+ * :func:`serd.uri` - A URI, like "http://example.org", which would be
+ ``<http://example.org>`` in Turtle.
+
+Convenience Constructors
+------------------------
+
+ * :func:`serd.string` - A string literal with no language or datatype.
+ * :func:`serd.decimal` - An `xsd:decimal
+ <https://www.w3.org/TR/xmlschema-2/#decimal>`_ like "123.45".
+ * :func:`serd.double` - An `xsd:double
+ <https://www.w3.org/TR/xmlschema-2/#double>`_ like "1.2345E2".
+ * :func:`serd.float` - An `xsd:float
+ <https://www.w3.org/TR/xmlschema-2/#float>`_ like "1.2345E2".
+ * :func:`serd.integer` - An `xsd:integer
+ <https://www.w3.org/TR/xmlschema-2/#integer>`_ like "1234567".
+ * :func:`serd.boolean` - An `xsd:boolean
+ <https://www.w3.org/TR/xmlschema-2/#boolean>`_ like "true" or "false".
+ * :func:`serd.blob` - An `xsd:base64Binary
+ <https://www.w3.org/TR/xmlschema-2/#base64Binary>`_ like "aGVsbG8=".
+ * :func:`serd.resolved_uri` - A URI resolved against a base like "http://example.org/rel".
+ * :func:`serd.file_uri` - A file URI like "file:///doc.ttl".
+ * :func:`serd.relative_uri` - A relative URI reference like "foo/bar".
+
+Namespaces
+==========
+
+It is common to use many URIs that share a common prefix. The
+:class:`~serd.Namespace` utility class can be used to make code more readable
+and make mistakes less likely:
+
+>>> eg = serd.Namespace("http://example.org/")
+>>> print(eg.thing)
+http://example.org/thing
+
+.. testsetup:: *
+
+ eg = serd.Namespace("http://example.org/")
+
+Dictionary syntax can also be used:
+
+>>> print(eg["thing"])
+http://example.org/thing
+
+For convenience, namespaces also act like strings in many cases:
+
+>>> print(eg)
+http://example.org/
+>>> print(eg + "stringeyName")
+http://example.org/stringeyName
+
+Note that this class is just a simple syntactic convenience, it does not
+"remember" names and there is no corresponding C API.
+
+Statements
+==========
+
+A :class:`~serd.Statement` is a tuple of either 3 or 4 nodes: the subject,
+predicate, object, and optional graph. Statements declare that a subject has
+some property. The predicate identifies the property, and the object is its
+value.
+
+A statement is a bit like a very simple machine-readable sentence. The
+"subject" and "object" are as in natural language, and the predicate is like
+the verb, but more general. For example, we could make a statement in English
+about your intrepid author:
+
+ drobilla has the first name "David"
+
+We can break this statement into 3 pieces like so:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Subject
+ - Predicate
+ - Object
+ * - drobilla
+ - has the first name
+ - "David"
+
+To make a :class:`~serd.Statement` out of this, we need to define some URIs. In
+RDF, the subject and predicate must be *resources* with an identifier (for
+example, neither can be a string). Conventionally, predicate names do not
+start with "has" or similar words, since that would be redundant in this
+context. So, we assume that ``http://example.org/drobilla`` is the URI for
+drobilla, and ``http://example.org/firstName`` has been defined somewhere to be
+a property with the appropriate meaning, and can make an equivalent
+:class:`~serd.Statement`:
+
+>>> print(serd.Statement(eg.drobilla, eg.firstName, serd.string("David")))
+<http://example.org/drobilla> <http://example.org/firstName> "David"
+
+If you find this terminology confusing, it may help to think in terms of
+dictionaries instead. For example, the above can be thought of as equivalent
+to::
+
+ drobilla[firstName] = "David"
+
+or::
+
+ drobilla.firstName = "David"
+
+Accessing Fields
+----------------
+
+Statement fields can be accessed via named methods or array indexing:
+
+>>> statement = serd.Statement(eg.s, eg.p, eg.o, eg.g)
+>>> print(statement.subject())
+http://example.org/s
+>>> print(statement[serd.Field.SUBJECT])
+http://example.org/s
+>>> print(statement[0])
+http://example.org/s
+
+Graph
+-----
+
+The graph field can be used as a context to distinguish otherwise identical
+statements. For example, it is often set to the URI of the document that the
+statement was loaded from:
+
+>>> print(serd.Statement(eg.s, eg.p, eg.o, serd.uri("file:///doc.ttl")))
+<http://example.org/s> <http://example.org/p> <http://example.org/o> <file:///doc.ttl>
+
+The graph field is always accessible, but may be ``None``:
+
+ >>> triple = serd.Statement(eg.s, eg.p, eg.o)
+ >>> print(triple.graph())
+ None
+ >>> quad = serd.Statement(eg.s, eg.p, eg.o, eg.g)
+ >>> print(quad.graph())
+ http://example.org/g
+
+World
+=====
+
+So far, we have only used nodes and statements, which are simple independent
+objects. Higher-level facilities in serd require a :class:`~serd.World` which
+represents the global library state.
+
+A program typically uses just one world, which can be constructed with no
+arguments::
+
+ world = serd.World()
+
+.. testsetup:: *
+
+ world = serd.World()
+
+Note that the world is not a database, it only manages a small amount of
+library state for things like configuration and logging.
+
+All "global" state is handle explicitly via the world. Serd does not contain
+any static mutable data, making it suitable for use in modules or plugins. If
+multiple worlds *are* used in a single program, they must never be mixed:
+objects "inside" one world can not be used with objects inside another.
+
+Generating Blanks
+-----------------
+
+Blank nodes, or simply "blanks", are used for resources that do not have URIs.
+Unlike URIs, they are not global identifiers, and only have meaning within
+their local context (for example, a document). The world provides a method for
+automatically generating unique blank identifiers:
+
+>>> print(repr(world.get_blank()))
+serd.blank("b1")
+>>> print(repr(world.get_blank()))
+serd.blank("b2")
+
+Model
+=====
+
+A :class:`~serd.Model` is an indexed set of statements. A model can be used to
+store any set of data, from a few statements (for example, a protocol message),
+to an entire document, to a database with millions of statements.
+
+A model can be constructed and statements inserted manually using the
+:meth:`~serd.Model.insert` method. Tuple syntax is supported as a shorthand
+for creating statements:
+
+>>> model = serd.Model(world)
+>>> model.insert((eg.s, eg.p, eg.o1))
+>>> model.insert((eg.s, eg.p, eg.o2))
+>>> model.insert((eg.t, eg.p, eg.o3))
+
+.. testsetup:: model_manual
+
+ import serd
+ eg = serd.Namespace("http://example.org/")
+ world = serd.World()
+ model = serd.Model(world)
+ model.insert((eg.s, eg.p, eg.o1))
+ model.insert((eg.s, eg.p, eg.o2))
+ model.insert((eg.t, eg.p, eg.o3))
+
+Iterating over the model yields every statement:
+
+>>> for s in model: print(s)
+<http://example.org/s> <http://example.org/p> <http://example.org/o1>
+<http://example.org/s> <http://example.org/p> <http://example.org/o2>
+<http://example.org/t> <http://example.org/p> <http://example.org/o3>
+
+Familiar Pythonic collection operations work as you would expect:
+
+>>> print(len(model))
+3
+>>> print((eg.s, eg.p, eg.o4) in model)
+False
+>>> model += (eg.s, eg.p, eg.o4)
+>>> print((eg.s, eg.p, eg.o4) in model)
+True
+
+Pattern Matching
+----------------
+
+The :meth:`~serd.Model.ask` method can be used to check if a statement is in a
+model:
+
+>>> print(model.ask(eg.s, eg.p, eg.o1))
+True
+>>> print(model.ask(eg.s, eg.p, eg.s))
+False
+
+This method is more powerful than the ``in`` statement because it also does
+pattern matching. To check for a pattern, use `None` as a wildcard:
+
+>>> print(model.ask(eg.s, None, None))
+True
+>>> print(model.ask(eg.unknown, None, None))
+False
+
+The :meth:`~serd.Model.count` method works similarly, but instead returns the
+number of statements that match the pattern:
+
+>>> print(model.count(eg.s, None, None))
+3
+>>> print(model.count(eg.unknown, None, None))
+0
+
+Getting Values
+--------------
+
+Sometimes you are only interested in a single node, and it is cumbersome to
+first search for a statement and then get the node from it. The
+:meth:`~serd.Model.get` method provides a more convenient way to do this. To
+get a value, specify a triple pattern where exactly one field is ``None``. If
+a statement matches, then the node that "fills" the wildcard will be returned:
+
+>>> print(model.get(eg.t, eg.p, None))
+http://example.org/o3
+
+If multiple statements match the pattern, then the matching node from an
+arbitrary statement is returned. It is an error to specify more than one
+wildcard, excluding the graph.
+
+Erasing Statements
+------------------
+
+>>> model2 = model.copy()
+>>> for s in model2: print(s)
+<http://example.org/s> <http://example.org/p> <http://example.org/o1>
+<http://example.org/s> <http://example.org/p> <http://example.org/o2>
+<http://example.org/s> <http://example.org/p> <http://example.org/o4>
+<http://example.org/t> <http://example.org/p> <http://example.org/o3>
+
+Individual statements can be erased by value, again with tuple syntax supported
+for convenience:
+
+>>> model2.erase((eg.s, eg.p, eg.o1))
+>>> for s in model2: print(s)
+<http://example.org/s> <http://example.org/p> <http://example.org/o2>
+<http://example.org/s> <http://example.org/p> <http://example.org/o4>
+<http://example.org/t> <http://example.org/p> <http://example.org/o3>
+
+Many statements can be erased at once by erasing a range:
+
+>>> model2.erase(model2.range((eg.s, None, None)))
+>>> for s in model2: print(s)
+<http://example.org/t> <http://example.org/p> <http://example.org/o3>
+
+Saving Documents
+----------------
+
+Serd provides simple methods to save an entire model to a file or string, which
+are similar to functions in the standard Python ``json`` module.
+
+A model can be saved to a file with the :meth:`~serd.World.dump` method:
+
+.. doctest::
+ :options: +NORMALIZE_WHITESPACE
+
+ >>> world.dump(model, "out.ttl")
+ >>> print(open("out.ttl", "r").read())
+ <http://example.org/s>
+ <http://example.org/p> <http://example.org/o1> ,
+ <http://example.org/o2> ,
+ <http://example.org/o4> .
+ <BLANKLINE>
+ <http://example.org/t>
+ <http://example.org/p> <http://example.org/o3> .
+ <BLANKLINE>
+
+Similarly, a model can be written as a string with the :meth:`serd.World.dumps`
+method:
+
+.. doctest::
+ :options: +ELLIPSIS
+
+ >>> print(world.dumps(model))
+ <http://example.org/s>
+ ...
+
+Loading Documents
+-----------------
+
+There are also simple methods to load an entire model, again loosely following
+the standard Python ``json`` module.
+
+A model can be loaded from a file with the :meth:`~serd.World.load` method:
+
+>>> model3 = world.load("out.ttl")
+>>> print(model3 == model)
+True
+
+By default, the syntax type is determined by the file extension, and only
+:attr:`serd.ModelFlags.INDEX_SPO` will be set, so only ``(s p ?)`` and ``(s ?
+?)`` queries will be fast. See the method documentation for how to control
+things more precisely.
+
+Similarly, a model can be loaded from a string with the
+:meth:`~serd.World.loads` method:
+
+>>> ttl = "<{}> <{}> <{}> .".format(eg.s, eg.p, eg.o)
+>>> model4 = world.loads(ttl)
+>>> for s in model4: print(s)
+<http://example.org/s> <http://example.org/p> <http://example.org/o>
+
+File Cursor
+-----------
+
+When data is loaded from a file into a model with the flag
+:data:`~serd.ModelFlags.STORE_CURSORS`, each statement will have a *cursor*
+which describes the file name, line, and column where the statement originated:
+
+>>> model5 = world.load("out.ttl", model_flags=serd.ModelFlags.STORE_CURSORS)
+>>> for s in model5: print(s.cursor())
+out.ttl:2:47
+out.ttl:3:25
+out.ttl:4:25
+out.ttl:7:47
+
+Streaming Data
+==============
+
+More advanced input and output can be performed by using the
+:class:`~serd.Reader` and :class:`~serd.Writer` classes directly. The Reader
+produces an :class:`~serd.Event` stream which describes the content of the
+file, and the Writer consumes such a stream and writes syntax.
+
+Reading Files
+-------------
+
+The reader reads from a source, which should be a :class:`~serd.FileSource`
+to read from a file. Parsed input is sent to a sink, which is
+called for each event:
+
+.. testcode::
+
+ def sink(event):
+ print(event)
+
+ reader = serd.Reader(world, serd.Syntax.TURTLE, 0, sink, 4096)
+ with reader.open(serd.FileSource("out.ttl")) as context:
+ context.read_document()
+
+.. testoutput::
+ :options: +ELLIPSIS
+
+ serd.Event.statement(serd.Statement(serd.uri("http://example.org/s"), serd.uri("http://example.org/p"), serd.uri("http://example.org/o1"), serd.Cursor(serd.uri("out.ttl"), 2, 47)))
+ ...
+
+For more advanced use cases that keep track of state, the sink can be a custom
+:class:`~serd.Sink` with a call operator:
+
+.. testcode::
+
+ class MySink(serd.Sink):
+ def __init__(self):
+ super().__init__()
+ self.events = []
+
+ def __call__(self, event: serd.Event) -> serd.Status:
+ self.events += [event]
+ return serd.Status.SUCCESS
+
+ sink = MySink()
+ reader = serd.Reader(world, serd.Syntax.TURTLE, 0, sink, 4096)
+ with reader.open(serd.FileSource("out.ttl")) as context:
+ context.read_document()
+
+ print(sink.events[0])
+
+.. testoutput::
+
+ serd.Event.statement(serd.Statement(serd.uri("http://example.org/s"), serd.uri("http://example.org/p"), serd.uri("http://example.org/o1"), serd.Cursor(serd.uri("out.ttl"), 2, 47)))
+
+Reading Strings
+---------------
+
+To read from a string, use a :class:`~serd.StringSource` with the reader:
+
+.. testcode::
+
+ ttl = """
+ @base <http://drobilla.net/> .
+ @prefix eg: <http://example.org/> .
+ <sw/serd> eg:name "Serd" .
+ """
+
+ def sink(event):
+ print(event)
+
+ reader = serd.Reader(world, serd.Syntax.TURTLE, 0, sink, 4096)
+ with reader.open(serd.StringSource(ttl)) as context:
+ context.read_document()
+
+.. testoutput::
+
+ serd.Event.base("http://drobilla.net/")
+ serd.Event.prefix("eg", "http://example.org/")
+ serd.Event.statement(serd.Statement(serd.uri("sw/serd"), serd.curie("eg:name"), serd.string("Serd"), serd.Cursor(serd.string("string"), 4, 24)))
+
+Reading into a Model
+--------------------
+
+To read new data into an existing model, use an :class:`~serd.Inserter` as a sink:
+
+.. testcode::
+
+ ttl = """
+ @prefix eg: <http://example.org/> .
+ eg:newSubject eg:p eg:o .
+ """
+
+ env = serd.Env()
+ sink = model.inserter(env)
+ reader = serd.Reader(world, serd.Syntax.TURTLE, 0, sink, 4096)
+ with reader.open(serd.StringSource(ttl)) as context:
+ context.read_document()
+
+ for s in model: print(s)
+
+.. testoutput::
+
+ <http://example.org/newSubject> <http://example.org/p> <http://example.org/o>
+ <http://example.org/s> <http://example.org/p> <http://example.org/o1>
+ <http://example.org/s> <http://example.org/p> <http://example.org/o2>
+ <http://example.org/s> <http://example.org/p> <http://example.org/o4>
+ <http://example.org/t> <http://example.org/p> <http://example.org/o3>
+
+Writing Files
+-------------
+
+.. testcode::
+
+ env = serd.Env()
+ byte_sink = serd.ByteSink(filename="written.ttl")
+ writer = serd.Writer(world, serd.Syntax.TURTLE, 0, env, byte_sink)
+ st = model.all().serialise(writer.sink(), 0)
+ writer.finish()
+ byte_sink.close()
+ print(open("written.ttl", "r").read())
+
+.. testoutput::
+ :options: +NORMALIZE_WHITESPACE
+
+ <http://example.org/newSubject>
+ <http://example.org/p> <http://example.org/o> .
+
+ <http://example.org/s>
+ <http://example.org/p> <http://example.org/o1> ,
+ <http://example.org/o2> ,
+ <http://example.org/o4> .
+
+ <http://example.org/t>
+ <http://example.org/p> <http://example.org/o3> .
diff --git a/bindings/python/reference.rst b/bindings/python/reference.rst
new file mode 100644
index 00000000..0b64037e
--- /dev/null
+++ b/bindings/python/reference.rst
@@ -0,0 +1,8 @@
+=============
+API Reference
+=============
+
+.. automodule:: serd
+ :members:
+ :undoc-members:
+ :inherited-members: