Skip to content

Docs improvements #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ jobs:
if: success() || failure()
run: pytest --cov=src --cov-report=lcov

- name: Check quickstart, including installation
if: success() || failure()
run: bash ./docs/source/quickstart/test_quickstart_example.sh

coverage:
runs-on: ubuntu-latest
needs: [base_coverage, test]
Expand Down
30 changes: 0 additions & 30 deletions docs/source/dependencies.rst

This file was deleted.

23 changes: 23 additions & 0 deletions docs/source/dependencies/dependencies.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

Dependencies
============

Simple actions depend only on their input parameters and the :class:`~labthings_fastapi.thing.Thing` on which they are defined. However, it's quite common to need something else, for example accessing another :class:`~labthings_fastapi.thing.Thing` instance on the same LabThings server. There are two important principles to bear in mind here:

* Other :class:`~labthings_fastapi.thing.Thing` instances should be accessed using a :class:`~labthings_fastapi.client.in_server.DirectThingClient` subclass if possible. This creates a wrapper object that should work like a :class:`~labthings_fastapi.client.ThingClient`, meaning your code should work either on the server or in a client script. This makes the code much easier to debug.
* LabThings uses the FastAPI "dependency injection" mechanism, where you specify what's needed with type hints, and the argument is supplied automatically at run-time. You can see the `FastAPI documentation`_ for more information.

LabThings provides a shortcut to create the annotated type needed to declare a dependency on another :class:`~labthings_fastapi.thing.Thing`, with the function :func:`~labthings_fastapi.dependencies.thing.direct_thing_client_dependency`. This generates a type annotation that you can use when you define your actions, that will supply a client object when the action is called.

:func:`~labthings_fastapi.dependencies.thing.direct_thing_client_dependency` takes a :func:`~labthings_fastapi.thing.Thing` class and a path as arguments: these should match the configuration of your LabThings server. Optionally, you can specify the actions that you're going to use. The default behaviour is to make all actions available, however it is more efficient to specify only the actions you will use.

Dependencies are added recursively - so if you depend on another Thing, and some of its actions have their own dependencies, those dependencies are also added to your action. Using the ``actions`` argument means you only need the dependencies of the actions you are going to use, which is more efficient.

.. literalinclude:: example.py
:language: python

In the example above, the :func:`increment_counter` action on :class:`TestThing` takes a :class:`MyThing` as an argument. When the action is called, the ``my_thing`` argument is supplied automatically. The argument is not the :class:`MyThing` instance, instead it is a wrapper class (a dynamically generated :class:`~labthings_fastapi.client.in_server.DirectThingClient` subclass). The wrapper should have the same signature as a :class:`~labthings_fastapi.client.ThingClient`. This means any dependencies of actions on the :class:`MyThing` are automatically supplied, so you only need to worry about the arguments that are not dependencies. The aim of this is to ensure that the code you write for your :class:`Thing` is as similar as possible to the code you'd write if you were using it through the Python client module.

If you need access to the actual Python object (e.g. you need to access methods that are not decorated as actions), you can use the :func:`~labthings_fastapi.dependencies.raw_thing.raw_thing_dependency` function instead. This will give you the actual Python object, but you will need to supply all the arguments of the actions, including dependencies, yourself.

.. _`FastAPI documentation`: https://fastapi.tiangolo.com/tutorial/dependencies/
26 changes: 26 additions & 0 deletions docs/source/dependencies/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from labthings_fastapi.thing import Thing
from labthings_fastapi.decorators import thing_action
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
from labthings_fastapi.example_things import MyThing
from labthings_fastapi.server import ThingServer

MyThingDep = direct_thing_client_dependency(MyThing, "/mything/")


class TestThing(Thing):
"""A test thing with a counter property and a couple of actions"""

@thing_action
def increment_counter(self, my_thing: MyThingDep) -> None:
"""Increment the counter on another thing"""
my_thing.increment_counter()


server = ThingServer()
server.add_thing(MyThing(), "/mything/")
server.add_thing(TestThing(), "/testthing/")

if __name__ == "__main__":
import uvicorn

uvicorn.run(server.app, port=5000)
4 changes: 2 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Welcome to labthings-fastapi's documentation!
:caption: Contents:

core_concepts.rst
quickstart.rst
dependencies.rst
quickstart/quickstart.rst
dependencies/dependencies.rst

apidocs/index

Expand Down
57 changes: 0 additions & 57 deletions docs/source/quickstart.rst

This file was deleted.

42 changes: 42 additions & 0 deletions docs/source/quickstart/counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import time
from labthings_fastapi.thing import Thing
from labthings_fastapi.decorators import thing_action
from labthings_fastapi.descriptors import PropertyDescriptor


class TestThing(Thing):
"""A test thing with a counter property and a couple of actions"""

@thing_action
def increment_counter(self) -> None:
"""Increment the counter property

This action doesn't do very much - all it does, in fact,
is increment the counter (which may be read using the
`counter` property).
"""
self.counter += 1

@thing_action
def slowly_increase_counter(self) -> None:
"""Increment the counter slowly over a minute"""
for i in range(60):
time.sleep(1)
self.increment_counter()

counter = PropertyDescriptor(
model=int, initial_value=0, readonly=True, description="A pointless counter"
)


if __name__ == "__main__":
from labthings_fastapi.server import ThingServer
import uvicorn

server = ThingServer()

# The line below creates a TestThing instance and adds it to the server
server.add_thing(TestThing(), "/counter/")

# We run the server using `uvicorn`:
uvicorn.run(server.app, port=5000)
11 changes: 11 additions & 0 deletions docs/source/quickstart/counter_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from labthings_fastapi.client import ThingClient

counter = ThingClient.from_url("http://localhost:5000/counter/")

v = counter.counter
print(f"The counter value was {v}")

counter.increment_counter()

v = counter.counter
print(f"After incrementing, the counter value was {v}")
5 changes: 5 additions & 0 deletions docs/source/quickstart/example_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"things": {
"example": "labthings_fastapi.example_things:MyThing"
}
}
52 changes: 52 additions & 0 deletions docs/source/quickstart/quickstart.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Quick start
===========

You can install `labthings-fastapi` using `pip`. We recommend you create a virtual environment, for example:


.. literalinclude:: quickstart_example.sh
:language: bash
:start-after: BEGIN venv
:end-before: END venv

then install labthings with:

.. literalinclude:: quickstart_example.sh
:language: bash
:start-after: BEGIN install
:end-before: END install

To define a simple example ``Thing``, paste the following into a python file, ``counter.py``:

.. literalinclude:: counter.py
:language: python

``counter.py`` defines the ``TestThing`` class, and then runs a LabThings server in its ``__name__ == "__main__"`` block. This means we should be able to run the server with:


.. literalinclude:: quickstart_example.sh
:language: bash
:start-after: BEGIN serve
:end-before: END serve

Visiting http://localhost:5000/counter/ will show the thing description, and you can interact with the actions and properties using the Swagger UI at http://localhost:5000/docs/.

You can also interact with it from another Python instance, for example by running:

.. literalinclude:: counter_client.py
:language: python

It's best to write ``Thing`` subclasses in Python packages that can be imported. This makes them easier to re-use and distribute, and also allows us to run a LabThings server from the command line, configured by a configuration file. An example config file is below:

.. literalinclude:: example_config.json
:language: JSON

Paste this into ``example_config.json`` and then run a server using:

.. code-block:: bash

labthings-server -c example_config.json

Bear in mind that this won't work if `counter.py` above is still running - both will try to use port 5000.

As before, you can visit http://localhost:5000/docs or http://localhost:5000/example/ to see the OpenAPI docs or Thing Description, and you can use the Python client module with the second of those URLs.
14 changes: 14 additions & 0 deletions docs/source/quickstart/quickstart_example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
echo "Setting up environemnt"
# BEGIN venv
python -m venv .venv --prompt labthings
source .venv/bin/activate # or .venv/Scripts/activate on Windows
# END venv
echo "Installing labthings-fastapi"
# BEGIN install
pip install labthings-fastapi[server]
# END install
echo "running example"
# BEGIN serve
python counter.py
# END serve
echo $! > example_server.pid
61 changes: 61 additions & 0 deletions docs/source/quickstart/test_quickstart_example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# cd to this directory
cd "${BASH_SOURCE%/*}/"
rm -rf .venv
# Override the virtual environment so we use the package
# from this repo, not from pypi
python -m venv .venv
source .venv/bin/activate
pip install ../../..
# Run the quickstart code in the background
bash "quickstart_example.sh" 2>&1 &
quickstart_example_pid=$!
echo "Spawned example with PID $quickstart_example_pid"

function killserver {
# Stop the server that we spawned.
children=$(ps -o pid= --ppid "$quickstart_example_pid")
kill $children
echo "Killed spawned processes: $children"

wait
}
trap killserver EXIT

# Wait for it to respond
# Loop until the command is successful or the maximum number of attempts is reached
ret=7
attempt_num=0
while [[ $ret == 7 ]]; do # && [ $attempt_num -le 50 ]
# Execute the command
ret=0
curl -sf -m 10 http://localhost:5000/counter/counter || ret=$?
if [[ $ret == 7 ]]; then
echo "Curl didn't connect on attempt $attempt_num"

# Check the example process hasn't died
ps $quickstart_example_pid > /dev/null
if [[ $? != 0 ]]; then
echo "Child process (the server example) died without responding."
exit -1
fi

attempt_num=$(( attempt_num + 1 ))
sleep 1
fi
done
echo "Final return value $ret on attempt $attempt_num"

if [[ $ret == 0 ]]; then
echo "Success"
else
echo "Curl returned $ret, likely something went wrong."
exit -1
fi

# Check the Python client code
echo "Running Python client code"
(source .venv/bin/activate && python counter_client.py)
if [[ $? != 0 ]]; then
echo "Python client code did not run OK."
exit -1
fi
28 changes: 28 additions & 0 deletions tests/test_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from pathlib import Path
from runpy import run_path
from test_server_cli import MonitoredProcess
from fastapi.testclient import TestClient
from labthings_fastapi.client import ThingClient


this_file = Path(__file__)
repo = this_file.parents[1]
docs = repo / "docs" / "source"


def run_quickstart_counter():
# A server is started in the `__name__ == "__main__" block`
run_path(docs / "quickstart" / "counter.py")


def test_quickstart_counter():
"""Check we can create a server from the command line"""
p = MonitoredProcess(target=run_quickstart_counter)
p.run_monitored(terminate_outputs=["Application startup complete"])


def test_dependency_example():
globals = run_path(docs / "dependencies" / "example.py", run_name="not_main")
with TestClient(globals["server"].app) as client:
testthing = ThingClient.from_url("/testthing/", client=client)
testthing.increment_counter()