Skip to content

Commit 8e714c8

Browse files
authored
Merge pull request #66 from labthings/docs
Documentation with Sphinx
2 parents 20094ff + 7e07ca2 commit 8e714c8

38 files changed

+433
-71
lines changed

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
[![codecov](https://codecov.io/gh/rwb27/labthings-fastapi/branch/main/graph/badge.svg?token=IR4QNA8X6M)](https://codecov.io/gh/rwb27/labthings-fastapi)
22

33
# labthings-fastapi
4-
An experimental implementation of a LabThings server using fastapi.
54

6-
This is currently an incomplete implementation of the WoT specification, and will gradually grow as required to support the OpenFlexure server.
5+
A FastAPI based library to implement a [Web of Things] interface for laboratory hardware using Python. This is a ground-up rewrite of [python-labthings], replacing Flask 1 and Marshmallow with FastAPI and Pydantic. It is the underlying framework for v3 of the [OpenFlexure Microscope software].
6+
7+
Features include:
8+
9+
* Better alignment with the Web of Things standard:
10+
- `Extensions` are gone, everything is now a `Thing`
11+
- `Thing`s are classes, with properties and actions defined exactly once
12+
- Various improvements to TD generation and validation with `pydantic`
13+
* Cleaner API
14+
- Datatypes of action input/outputs and properties are defined with Python type hints
15+
- Actions are defined exactly once, as a method of a `Thing` class
16+
- Properties and actions are declared using decorators (or descriptors if that's preferred)
17+
- Dependency injection is used to manage relationships between Things and dependency on the server
18+
* Async HTTP handling
19+
- Starlette (used by FastAPI) can handle requests asynchronously - potential for websockets/events (not used much yet)
20+
- `Thing` code is still, for now, threaded. I intend to make it possible to write async things in the future, but don't intend it to become mandatory
21+
* Smaller codebase
22+
- FastAPI more or less completely eliminates OpenAPI generation code from our codebase
23+
- Thing Description generation is very much simplified by the new structure (multiple Things instead of one massive Thing with many extensions)
24+
* Extensive testing
25+
- FastAPI/Starlette have nice test provision, so the vast majority of the codebase is already covered
26+
727

828
## Installation
929

10-
You can install this repository with `pip`, either clone it and run `pip install -e .[dev]` to work on it, or just `pip install https://gitlab.com/rwb27/labthings-fastapi.git`.
30+
You can install this repository with `pip`, either clone it and run `pip install -e .[dev]` to work on it, or just `pip install https://gitlab.com/rwb27/labthings-fastapi.git`. It will be published on PyPI in the near future, initially as `labthings-fastapi`. It may at some point be renamed to `labthings` v2.
1131

1232
## Developer notes
1333

@@ -16,3 +36,7 @@ The code is linted with `ruff .`, type checked with `mypy src`, and tested with
1636
## Demo
1737

1838
See the [examples folder](./examples/) for a runnable demo.
39+
40+
[Web of Things]: https://www.w3.org/WoT/
41+
[python-labthings]: https://github.com/labthings/python-labthings/
42+
[OpenFlexure Microscope software]: https://gitlab.com/openflexure/openflexure-microscope-server/

docs/Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Minimal makefile for Sphinx documentation
2+
#
3+
4+
# You can set these variables from the command line, and also
5+
# from the environment for the first two.
6+
SPHINXOPTS ?=
7+
SPHINXBUILD ?= sphinx-build
8+
SOURCEDIR = source
9+
BUILDDIR = build
10+
11+
# Put it first so that "make" without argument is like "make help".
12+
help:
13+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14+
15+
.PHONY: help Makefile
16+
17+
# Catch-all target: route all unknown targets to Sphinx using the new
18+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19+
%: Makefile
20+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

docs/make.bat

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@ECHO OFF
2+
3+
pushd %~dp0
4+
5+
REM Command file for Sphinx documentation
6+
7+
if "%SPHINXBUILD%" == "" (
8+
set SPHINXBUILD=sphinx-build
9+
)
10+
set SOURCEDIR=source
11+
set BUILDDIR=build
12+
13+
%SPHINXBUILD% >NUL 2>NUL
14+
if errorlevel 9009 (
15+
echo.
16+
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17+
echo.installed, then set the SPHINXBUILD environment variable to point
18+
echo.to the full path of the 'sphinx-build' executable. Alternatively you
19+
echo.may add the Sphinx directory to PATH.
20+
echo.
21+
echo.If you don't have Sphinx installed, grab it from
22+
echo.https://www.sphinx-doc.org/
23+
exit /b 1
24+
)
25+
26+
if "%1" == "" goto help
27+
28+
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29+
goto end
30+
31+
:help
32+
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33+
34+
:end
35+
popd

docs/requirements.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sphinx-autodoc2==0.5.0
2+
mdit-py-plugins>=0.3.4
3+
myst-parser>=3.0.1, <4
4+
sphinx-rtd-theme==2.0.0

docs/requirements.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
alabaster==0.7.16
2+
astroid==3.2.1
3+
Babel==2.15.0
4+
certifi==2024.2.2
5+
charset-normalizer==3.3.2
6+
colorama==0.4.6
7+
docutils==0.20.1
8+
idna==3.7
9+
imagesize==1.4.1
10+
Jinja2==3.1.4
11+
markdown-it-py==3.0.0
12+
MarkupSafe==2.1.5
13+
mdit-py-plugins==0.4.1
14+
mdurl==0.1.2
15+
myst-parser==3.0.1
16+
packaging==24.0
17+
Pygments==2.18.0
18+
PyYAML==6.0.1
19+
requests==2.31.0
20+
snowballstemmer==2.2.0
21+
Sphinx==7.3.7
22+
sphinx-autodoc2==0.5.0
23+
sphinx-rtd-theme==2.0.0
24+
sphinxcontrib-applehelp==1.0.8
25+
sphinxcontrib-devhelp==1.0.6
26+
sphinxcontrib-htmlhelp==2.0.5
27+
sphinxcontrib-jquery==4.1
28+
sphinxcontrib-jsmath==1.0.1
29+
sphinxcontrib-qthelp==1.0.7
30+
sphinxcontrib-serializinghtml==1.1.10
31+
typing_extensions==4.11.0
32+
urllib3==2.2.1

docs/source/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build/
2+
.venv/
3+
/apidocs/

docs/source/conf.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Configuration file for the Sphinx documentation builder.
2+
#
3+
# For the full list of built-in configuration values, see the documentation:
4+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
5+
6+
# -- Project information -----------------------------------------------------
7+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8+
9+
project = "labthings-fastapi"
10+
copyright = "2024, Richard Bowman"
11+
author = "Richard Bowman"
12+
release = "0.0.1"
13+
14+
# -- General configuration ---------------------------------------------------
15+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
16+
17+
extensions = [
18+
"myst_parser",
19+
"sphinx.ext.intersphinx",
20+
# "sphinx.ext.napoleon",
21+
"autodoc2",
22+
"sphinx_rtd_theme",
23+
]
24+
25+
templates_path = ["_templates"]
26+
exclude_patterns = []
27+
28+
autodoc2_packages = ["../../src/labthings_fastapi"]
29+
autodoc2_render_plugin = "myst"
30+
31+
# autoapi_dirs = ["../../src/labthings_fastapi"]
32+
# autoapi_ignore = []
33+
# autoapi_generate_api_docs = True
34+
# autoapi_keep_files = True
35+
36+
# -- Options for HTML output -------------------------------------------------
37+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
38+
39+
html_theme = "sphinx_rtd_theme"
40+
html_static_path = ["_static"]
41+
42+
intersphinx_mapping = {
43+
"python": ("https://docs.python.org/3", None),
44+
"fastapi": ("https://fastapi.tiangolo.com", None),
45+
}
46+
47+
myst_enable_extensions = ["fieldlist"]

docs/source/core_concepts.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Core Concepts
2+
=============
3+
4+
LabThings is rooted in the `W3C Web of Things standards <WoT>`_. Using IP networking in labs is not itself new, though perhaps under-used. However lack of proper standardisation has stiffled widespread adoption. LabThings, rather than try to introduce new competing standards, uses the architecture and terminology introduced by the W3C Web of Things. A full description of the core architecture can be found in the `Web of Things (WoT) Architecture <https://www.w3.org/TR/wot-architecture/#sec-wot-architecture>`_ document. However, a brief outline of the concepts relevant to `labthings-fastapi` is given below.
5+
6+
Thing
7+
---------
8+
9+
A `Thing` represents a piece of hardware or software. It could be a whole instrument (e.g. a microscope), a component within an instrument (e.g. a translation stage or camera), or a piece of software (e.g. code to tile together large area scans). `Thing`s in `labthings-fastapi` are Python classes that define Properties, Actions, and Events (see below). A Thing (sometimes called a "Web Thing") is defined by W3C as "an abstraction of a physical or a virtual entity whose metadata and interfaces are described by a WoT Thing description."
10+
11+
`labthings-fastapi` automatically generates a `Thing Description`_ to describe each `Thing`. Each function offered by the `Thing` is either a Property, Action, or Event. These are termed "interaction affordances" in WoT_ terminology.
12+
13+
Properties
14+
----------
15+
16+
As a rule of thumb, any attribute of your device that can be quickly read, or optionally written, should be a Property. For example, simple device settings, or status information (like a temperature) that takes negligible time to measure. Reading a property should never be a slow operation, as it is expected to be called frequently by clients. Properties are defined as "an Interaction Affordance that allows to read, write, or observe a state of the Thing" in the WoT_ standard. Similarly, writing to a property ought to be quick, and should not cause equipment to perform long-running operations. Properties are defined very similar to standard Python properties, using a decorator that adds them to the `Thing Description`_ and the HTTP API.
17+
18+
Actions
19+
-------
20+
21+
Actions generally correspond to making equipment (or software) do something. For example, starting a data acquisition, moving a stage, or changing a setting that requires a significant amount of time to complete. The key point here is that Actions are typically more complex in functionality than simply setting or getting a property. For example, they can set multiple properties simultaneously (for example, auto-exposing a camera), or they can manipulate the state of the Thing over time, for example starting a long-running data acquisition.
22+
23+
`labthings-fastapi` runs actions in background threads. This allows other actions and properties to be accessed while it is running. You define actions as methods of your `Thing` class using the decorator.
24+
25+
Events
26+
------
27+
28+
An event "describes an event source that pushes data asynchronously from the Thing to the Consumer. Here not state, but state transitions (i.e., events) are communicated. Events MAY be triggered through conditions that are not exposed as Properties."
29+
30+
Common examples are notifying clients when a Property is changed, or when an Action starts or finishes. However, Thing developers can introduce new Events such as warnings, status messages, and logs. For example, a device may emit an events when the internal temperature gets too high, or when an interlock is tripped. This Event can then be pushed to both users AND other Things, allowing automtic response to external conditions.
31+
32+
A good example of this might be having Things automatically pause data-acquisition Actions upon detection of an overheat or interlock Event from another Thing. Events are not currently implemented in `labthings-fastapi`, but are planned for future releases.
33+
34+
.. _WoT: https://www.w3.org/WoT/
35+
.. _Thing Description: https://www.w3.org/TR/wot-thing-description/

docs/source/index.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.. labthings-fastapi documentation master file, created by
2+
sphinx-quickstart on Wed May 15 16:34:51 2024.
3+
You can adapt this file completely to your liking, but it should at least
4+
contain the root `toctree` directive.
5+
6+
Welcome to labthings-fastapi's documentation!
7+
=============================================
8+
9+
.. toctree::
10+
:maxdepth: 2
11+
:caption: Contents:
12+
13+
core_concepts.rst
14+
quickstart.rst
15+
16+
apidocs/index
17+
18+
api.rst
19+
20+
`labthings-fastapi` implements a Web of Things interface for laboratory hardware using Python. This is a ground-up rewrite of python-labthings_, replacing Flask 1 and Marshmallow with FastAPI and Pydantic. It is the underlying framework for v3 of the `OpenFlexure Microscope software <https://gitlab.com/openflexure/openflexure-microscope-server/>`_.
21+
22+
Features include:
23+
24+
* Alignment with the `W3C Web of Things <https://www.w3.org/WoT/>`_ standard (see :doc:`core_concepts`)
25+
- Things are classes, with properties and actions defined exactly once
26+
- Various improvements to TD generation and validation with `pydantic`
27+
* Cleaner API
28+
- Datatypes of action input/outputs and properties are defined with Python type hints
29+
- Actions are defined exactly once, as a method of a `Thing` class
30+
- Properties and actions are declared using decorators (or descriptors if that's preferred)
31+
- Dependency injection is used to manage relationships between Things and dependency on the server
32+
* Async HTTP handling
33+
- Starlette (used by FastAPI) can handle requests asynchronously - potential for websockets/events (not used much yet)
34+
- `Thing` code is still, for now, threaded. I intend to make it possible to write async things in the future, but don't intend it to become mandatory
35+
* Smaller codebase
36+
- FastAPI more or less completely eliminates OpenAPI generation code from our codebase
37+
- Thing Description generation is very much simplified by the new structure (multiple Things instead of one massive Thing with many extensions)
38+
39+
40+
Installation
41+
------------
42+
43+
``pip install labthings-fastapi``
44+
45+
Indices and tables
46+
==================
47+
48+
* :ref:`genindex`
49+
* :ref:`modindex`
50+
* :ref:`search`
51+
52+
.. _python-labthings: https://github.com/labthings/python-labthings/

docs/source/quickstart.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Quick start
2+
===========
3+
4+
The fastest way to get started with `labthings-fastapi` is to try out one of the examples.
5+
6+
You can install `labthings-fastapi` using `pip`:
7+
8+
.. code-block:: bash
9+
10+
pip install labthings-fastapi
11+
12+
Then, paste the following into a python file, ``counter.py``:
13+
14+
.. code-block:: python
15+
16+
import time
17+
from labthings_fastapi.thing import Thing
18+
from labthings_fastapi.decorators import thing_action
19+
from labthings_fastapi.descriptors import PropertyDescriptor
20+
from labthings_fastapi.thing_server import ThingServer
21+
22+
23+
class TestThing(Thing):
24+
"""A test thing with a counter property and a couple of actions"""
25+
26+
@thing_action
27+
def increment_counter(self) -> None:
28+
"""Increment the counter property
29+
30+
This action doesn't do very much - all it does, in fact,
31+
is increment the counter (which may be read using the
32+
`counter` property).
33+
"""
34+
self.counter += 1
35+
36+
@thing_action
37+
def slowly_increase_counter(self) -> None:
38+
"""Increment the counter slowly over a minute"""
39+
for i in range(60):
40+
time.sleep(1)
41+
self.increment_counter()
42+
43+
counter = PropertyDescriptor(
44+
model=int, initial_value=0, readonly=True, description="A pointless counter"
45+
)
46+
47+
48+
server = ThingServer()
49+
server.add_thing(TestThing(), "/test")
50+
51+
You can then run this file with `uvicorn`:
52+
53+
.. code-block:: bash
54+
55+
uvicorn counter:app --reload
56+
57+
This will start a server on `http://localhost:8000` that serves the `TestThing` thing. Visiting `http://localhost:8000/test/` will show the thing description, and you can interact with the actions and properties using the Swagger UI at `http://localhost:8000/docs/`.

requirements.txt

-1.93 KB
Binary file not shown.

src/labthings_fastapi/actions/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,14 @@ def status(self) -> InvocationStatus:
108108
"""
109109
Current running status of the thread.
110110
111-
============== =============================================
112-
Status Meaning
113-
============== =============================================
114-
``pending`` Not yet started
115-
``running`` Currently in-progress
116-
``completed`` Finished without error
117-
``cancelled`` Thread stopped after a cancel request
118-
``error`` Exception occured in thread
119-
============== =============================================
111+
| Status | Meaning |
112+
|----------------|---------|
113+
| ``pending`` | Not yet started |
114+
| ``running`` | Currently in-progress |
115+
| ``completed`` | Finished without error |
116+
| ``cancelled`` | Thread stopped after a cancel request |
117+
| ``error`` | Exception occured in thread |
118+
120119
"""
121120
with self._status_lock:
122121
return self._status

src/labthings_fastapi/client/in_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
This module may get moved in the near future.
88
99
"""
10+
1011
from __future__ import annotations
1112
from functools import wraps
1213
import inspect

src/labthings_fastapi/decorators/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
If you have a complex datatype, it's recommended to use a `pydantic` model
1111
to describe it - this is often the case for complicated properties or events.
12-
For actions, a model is created automatically based on the function's
12+
For actions, a model is created automatically based on the function's
1313
signature: if you want to add descriptions or validators to individual
1414
arguments, you may use `pydantic.Field` to do this.
1515

0 commit comments

Comments
 (0)