Skip to content
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
25 changes: 25 additions & 0 deletions .codespellrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[codespell]

skip = *.git,
./.vscode,
*.pyc,
__pycache__,
./build,
./dist,
./docs/_build,
./htmlcov,
./src/openflexure_microscope_server/static,
.venv,
*.egg-info,
report.xml,
dev-requirements.txt,
coverage.yml,
coverage.lcov,
coverage.xml,
LICENSE,

# The regex allows it to also check snake case strings. This stops codespell doing
# autocorrection. If a lot of changes are needed this can be commented out.
# Having this does mean that contractions in variable names will be marked as spelt
# incorrectly.
regex = [a-zA-Z0-9\-'’]+
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
- name: Format with Ruff
run: ruff format --check .

- name: Check spelling
run: codespell .

- name: Lint with Flake8 (for docstrings)
if: ${{ contains('3.12,3.13', matrix.python) }}
# Flake8 crashes on Python < 3.12, so we exclude those versions.
Expand Down
26 changes: 4 additions & 22 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@ click==8.2.1
# rich-toolkit
# typer
# uvicorn
colorama==0.4.6
# via
# click
# pytest
# sphinx
# uvicorn
codespell==2.4.1
# via labthings-fastapi (pyproject.toml)
coverage==7.9.2
# via pytest-cov
dnspython==2.7.0
Expand All @@ -53,10 +49,6 @@ email-validator==2.2.0
# via
# fastapi
# pydantic
exceptiongroup==1.3.0
# via
# anyio
# pytest
fastapi==0.116.1
# via labthings-fastapi (pyproject.toml)
fastapi-cli==0.0.8
Expand Down Expand Up @@ -247,14 +239,6 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
starlette==0.47.1
# via fastapi
tomli==2.2.1
# via
# coverage
# flake8-pyproject
# mypy
# pydoclint
# pytest
# sphinx
typer==0.16.0
# via
# fastapi-cli
Expand All @@ -265,20 +249,16 @@ typing-extensions==4.14.1
# via
# labthings-fastapi (pyproject.toml)
# anyio
# astroid
# exceptiongroup
# fastapi
# mypy
# pydantic
# pydantic-core
# pydantic-extra-types
# referencing
# rich
# rich-toolkit
# starlette
# typer
# typing-inspection
# uvicorn
typing-inspection==0.4.1
# via pydantic-settings
ujson==5.10.0
Expand All @@ -292,6 +272,8 @@ uvicorn==0.35.0
# fastapi
# fastapi-cli
# fastapi-cloud-cli
uvloop==0.21.0
# via uvicorn
watchfiles==1.1.0
# via uvicorn
websockets==15.0.1
Expand Down
2 changes: 1 addition & 1 deletion docs/source/quickstart/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ You can also interact with it from another Python instance, for example by runni
.. 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:
It's best to write ``Thing`` subclasses in Python packages that can be imported. This makes them easier to reuse 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
Expand Down
2 changes: 1 addition & 1 deletion docs/source/quickstart/quickstart_example.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
echo "Setting up environemnt"
echo "Setting up environment"
# BEGIN venv
python -m venv .venv --prompt labthings
source .venv/bin/activate # or .venv/Scripts/activate on Windows
Expand Down
2 changes: 1 addition & 1 deletion docs/source/wot_core_concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Events

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."

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.
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 automatic response to external conditions.

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.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dev = [
"sphinx-rtd-theme",
"sphinx>=7.2",
"sphinx-autoapi",
"codespell",
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/client/in_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@
obj: Optional[DirectThingClient] = None,
_objtype: Optional[type[DirectThingClient]] = None,
):
if obj is None:
return self
return getattr(obj._wrapped_thing, self.name)

Check warning on line 122 in src/labthings_fastapi/client/in_server.py

View workflow job for this annotation

GitHub Actions / coverage

120-122 lines are not covered with tests

def __set__(self, obj: DirectThingClient, value: Any):
setattr(obj._wrapped_thing, self.name, value)
Expand All @@ -143,7 +143,7 @@
dictionary. This makes the assumption that, if a name is reused, it is
reused for the same dependency.

When names are re-used, we check if the values match. If not, this
When names are reused, we check if the values match. If not, this
exception is raised.
"""

Expand Down Expand Up @@ -293,7 +293,7 @@
else:
for affordance in ["property", "action", "event"]:
if hasattr(item, f"{affordance}_affordance"):
logging.warning(

Check warning on line 296 in src/labthings_fastapi/client/in_server.py

View workflow job for this annotation

GitHub Actions / coverage

296 line is not covered with tests
f"DirectThingClient doesn't support custom affordances, "
f"ignoring {name}"
)
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def thing_action(
:param \**kwargs: Keyword arguments are passed to the constructor
of `.ActionDescriptor`.

:return: Whether used with or without argumnts, the result is that
:return: Whether used with or without arguments, the result is that
the method is wrapped in an `.ActionDescriptor`, so it can be
called as usual, but will also be exposed over HTTP.
"""
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/dependencies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
example invoking actions or accessing properties on other `.Thing`\ s or
calling methods provided by the server.

:ref:`dependencies` are a `FastAPI concept`_ that is re-used in LabThings to allow
:ref:`dependencies` are a `FastAPI concept`_ that is reused in LabThings to allow
:ref:`actions` to request resources in a way that plays nicely with type hints
and is easy to intercept for testing.

Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/dependencies/blocking_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@
:param request: The `fastapi.Request` object, supplied by the :ref:`dependencies`
mechanism.

:return: the `anyio.from_thread.BlockingPortal` allowing access to te
:return: the `anyio.from_thread.BlockingPortal` allowing access to the
`.ThingServer`\ 's event loop.
"""
portal = find_thing_server(request.app).blocking_portal
assert portal is not None, RuntimeError(

Check warning on line 46 in src/labthings_fastapi/dependencies/blocking_portal.py

View workflow job for this annotation

GitHub Actions / coverage

45-46 lines are not covered with tests
"Could not get the blocking portal from the server."
# This should never happen, as the blocking portal is added
# and removed in `.ThingServer.lifecycle`.
)
return portal

Check warning on line 51 in src/labthings_fastapi/dependencies/blocking_portal.py

View workflow job for this annotation

GitHub Actions / coverage

51 line is not covered with tests


BlockingPortal = Annotated[
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/descriptors/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@

@overload
def __get__(self, obj: Literal[None], type=None) -> ActionDescriptor: # noqa: D105
...

Check warning on line 126 in src/labthings_fastapi/descriptors/action.py

View workflow job for this annotation

GitHub Actions / coverage

126 line is not covered with tests

@overload
def __get__(self, obj: Thing, type=None) -> Callable: # noqa: D105
...

Check warning on line 130 in src/labthings_fastapi/descriptors/action.py

View workflow job for this annotation

GitHub Actions / coverage

130 line is not covered with tests

def __get__(
self, obj: Optional[Thing], type: Optional[type[Thing]] = None
Expand Down Expand Up @@ -207,20 +207,20 @@
try:
runner = get_blocking_portal(obj)
if not runner:
thing_name = obj.__class__.__name__
msg = (

Check warning on line 211 in src/labthings_fastapi/descriptors/action.py

View workflow job for this annotation

GitHub Actions / coverage

210-211 lines are not covered with tests
f"Cannot emit action changed event. Is {thing_name} connected to "
"a running server?"
)
raise NotConnectedToServerError(msg)

Check warning on line 215 in src/labthings_fastapi/descriptors/action.py

View workflow job for this annotation

GitHub Actions / coverage

215 line is not covered with tests
runner.start_task_soon(
self.emit_changed_event_async,
obj,
status,
)
except Exception:

Check warning on line 221 in src/labthings_fastapi/descriptors/action.py

View workflow job for this annotation

GitHub Actions / coverage

221 line is not covered with tests
# TODO: in the unit test, the get_blocking_portal throws exception
...

Check warning on line 223 in src/labthings_fastapi/descriptors/action.py

View workflow job for this annotation

GitHub Actions / coverage

223 line is not covered with tests

async def emit_changed_event_async(self, obj: Thing, value: Any) -> None:
"""Notify subscribers that the action status has changed.
Expand Down Expand Up @@ -249,7 +249,7 @@

This function creates two functions to handle ``GET`` and ``POST``
requests to the action's endpoint, and adds them to the `fastapi.FastAPI`
aplication.
application.

:param app: The `fastapi.FastAPI` app to add the endpoint to.
:param thing: The `.Thing` to which the action is attached. Bear in
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/descriptors/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _observers_set(self, obj: Thing):
def emit_changed_event(self, obj: Thing, value: Any) -> None:
"""Notify subscribers that the property has changed.

This function is run when properties are upadated. It must be run from
This function is run when properties are updated. It must be run from
within a thread. This could be the `Invocation` thread of a running action, or
the property should be updated over via a client/http. It must be run from a
thread as it is communicating with the event loop via an `asyncio` blocking
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/example_things/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def action_with_only_kwargs(self, **kwargs: dict) -> None:


class ThingWithBrokenAffordances(Thing):
"""A Thing that raises exceptions in actions/properites."""
"""A Thing that raises exceptions in actions/properties."""

@thing_action
def broken_action(self):
Expand Down
6 changes: 3 additions & 3 deletions src/labthings_fastapi/outputs/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ class Blob(BaseModel):
separate HTTP request.

`.Blob` objects created by a `.ThingClient` contain a URL pointing to the
data, which will be downloaded when it is requred.
data, which will be downloaded when it is required.

`.Blob` objects that store their data in a file or in memory will have the
``href`` attribute set to the special value `blob://local`.
Expand Down Expand Up @@ -457,7 +457,7 @@ def content(self) -> bytes:
this property to download the output.

This property is read-only. You should also only read it once, as no
guarantees are given about cacheing - reading it many times risks
guarantees are given about caching - reading it many times risks
reading the file from disk many times, or re-downloading an artifact.

:return: a `bytes` object containing the data.
Expand Down Expand Up @@ -714,7 +714,7 @@ async def blob_serialisation_context_manager(

In order to serialise a `.Blob` to a JSON-serialisable dictionary, we must
add it to the `.BlobDataManager` and use that to generate a URL. This
requres that the serialisation code (which may be nested deep within a
requires that the serialisation code (which may be nested deep within a
`pydantic.BaseModel`) has access to the `.BlobDataManager` and also the
`fastapi.Request.url_for` method. At time of writing, there was not an
obvious way to pass these functions in to the serialisation code.
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/outputs/mjpeg_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ async def buffer_for_reading(self, i: int) -> AsyncIterator[bytes]:
after any ``with`` statement has finished).

Using a context manager is intended to allow future versions of this
code to manage access to the ringbuffer (e.g. allowing buffer re-use).
code to manage access to the ringbuffer (e.g. allowing buffer reuse).
Currently, buffers are always created as fresh `bytes` objects, so
this context manager does not provide additional functionality
over `.MJPEGStream.ringbuffer_entry`.
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/server/fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
</style>
</head>
<body>
<h1>LabThings Could't Load</h1>
<h1>LabThings Couldn't Load</h1>
<p>Something went wrong when setting up your LabThings server.</p>
<p>Please check your configuration and try again.</p>
<p>More details may be shown below:</p>
Expand Down
2 changes: 1 addition & 1 deletion src/labthings_fastapi/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def thing_state(self) -> Mapping:
it requires calls e.g. to a serial instrument, bear in mind it may be called
quite often and shouldn't take too long.

Some measure of cacheing here is a nice aim for the future, but not yet
Some measure of caching here is a nice aim for the future, but not yet
implemented.
"""
if self._labthings_thing_state is None:
Expand Down
4 changes: 2 additions & 2 deletions src/labthings_fastapi/thing_description/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def is_a_reference(d: JSONSchema) -> bool:
def look_up_reference(reference: str, d: JSONSchema) -> JSONSchema:
"""Look up a reference in a JSONSchema.

JSONSchema allows references, where chunks of JSON may be re-used.
JSONSchema allows references, where chunks of JSON may be reused.
Thing Description does not allow references, so we need to resolve
them and paste them in-line.

Expand Down Expand Up @@ -93,7 +93,7 @@ def is_an_object(d: JSONSchema) -> bool:
def convert_object(d: JSONSchema) -> JSONSchema:
"""Convert an object from JSONSchema to Thing Description.

Convert JSONSchema objets to Thing Description datatypes.
Convert JSONSchema objects to Thing Description datatypes.

Currently, this deletes the ``additionalProperties`` keyword, which is
not supported by Thing Description.
Expand Down
4 changes: 2 additions & 2 deletions tests/test_example_thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ def test_thing_with_broken_affordances():
thing.broken_property()


def test_thing_that_cant_instantiate():
def test_thing_that_cannot_instantiate():
with pytest.raises(Exception):
ThingThatCantInstantiate()


def test_thing_that_cant_start():
def test_thing_that_cannot_start():
thing = ThingThatCantStart()
assert isinstance(thing, ThingThatCantStart)
with pytest.raises(Exception):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ def test_fallback_with_server():


def test_fallback_with_log():
app.log_history = "Fake log conetent"
app.log_history = "Fake log content"
with TestClient(app) as client:
response = client.get("/")
html = response.text
assert "Something went wrong" in html
assert "No logging info available" not in html
assert "<p>Logging info</p>" in html
assert "Fake log conetent" in html
assert "Fake log content" in html
4 changes: 2 additions & 2 deletions tests/test_server_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def test_invalid_thing():
def test_fallback():
"""test the fallback option

startd a dummy server with an error page -
started a dummy server with an error page -
it terminates once the server starts.
"""
config_json = json.dumps(
Expand All @@ -155,7 +155,7 @@ def test_invalid_config():
check_serve_from_cli(["-c", "non_existent_file.json"])


def test_thing_that_cant_start():
def test_thing_that_cannot_start():
"""Check it fails for a thing that can't start"""
config_json = json.dumps(
{
Expand Down