diff --git a/README.md b/README.md index 9395a8ec..93dbb005 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ All changes to the codebase should go via pull requests, and should only be merg Dependencies are defined in `pyproject.toml` and can be compiled to `dev-requirements.txt` with: ``` -uv pip compile --extra dev pyproject.toml --output-file dev-requirements.txt +uv pip compile dev-requirements.in -o dev-requirements.txt ``` If you're not using `uv`, just regular `pip-compile` from `pip-tools` should do the same thing. diff --git a/dev-requirements.in b/dev-requirements.in new file mode 100644 index 00000000..aefbcb6d --- /dev/null +++ b/dev-requirements.in @@ -0,0 +1 @@ +-e .[dev] diff --git a/dev-requirements.txt b/dev-requirements.txt index 8b910ac6..d5a51512 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,7 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --extra dev pyproject.toml --output-file dev-requirements.txt +# uv pip compile dev-requirements.in -o dev-requirements.txt +-e . + # via -r dev-requirements.in alabaster==1.0.0 # via sphinx annotated-doc==0.0.4 @@ -8,10 +10,10 @@ annotated-doc==0.0.4 # typer annotated-types==0.7.0 # via pydantic -anyio==4.13.0 +anyio==4.14.0 # via - # labthings-fastapi (pyproject.toml) # httpx + # labthings-fastapi # starlette # watchfiles apeye==1.4.1 @@ -29,11 +31,11 @@ autodocsumm==0.2.15 # via sphinx-toolbox babel==2.18.0 # via sphinx -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 # via sphinx-toolbox cachecontrol==0.14.4 # via sphinx-toolbox -certifi==2026.5.20 +certifi==2026.6.17 # via # httpcore # httpx @@ -49,8 +51,8 @@ click==8.4.1 # rich-toolkit # uvicorn codespell==2.4.2 - # via labthings-fastapi (pyproject.toml) -coverage==7.14.1 + # via labthings-fastapi +coverage==7.14.3 # via pytest-cov detect-installer==0.1.0 # via fastapi-cloud-cli @@ -83,30 +85,30 @@ exceptiongroup==1.3.1 # anyio # pytest fastapi==0.135.4 - # via labthings-fastapi (pyproject.toml) -fastapi-cli==0.0.24 + # via labthings-fastapi +fastapi-cli==0.0.27 # via fastapi -fastapi-cloud-cli==0.18.0 +fastapi-cloud-cli==0.20.0 # via fastapi-cli fastar==0.11.0 # via fastapi-cloud-cli -filelock==3.29.0 +filelock==3.29.4 # via # cachecontrol # sphinx-toolbox flake8==7.3.0 # via - # labthings-fastapi (pyproject.toml) # flake8-pyproject # flake8-rst # flake8-rst-docstrings + # labthings-fastapi # pydoclint flake8-pyproject==1.2.4 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi flake8-rst==0.8.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi flake8-rst-docstrings==0.4.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi h11==0.16.0 # via # httpcore @@ -119,10 +121,10 @@ httptools==0.8.0 # via uvicorn httpx==0.28.1 # via - # labthings-fastapi (pyproject.toml) # fastapi # fastapi-cloud-cli -idna==3.16 + # labthings-fastapi +idna==3.18 # via # anyio # apeye-core @@ -146,7 +148,7 @@ jinja2==3.1.6 # sphinx-jinja2-compat jsonschema==4.26.0 # via - # labthings-fastapi (pyproject.toml) + # labthings-fastapi # sphobjinv jsonschema-specifications==2025.9.1 # via jsonschema @@ -162,16 +164,16 @@ mccabe==0.7.0 # via flake8 mdurl==0.1.2 # via markdown-it-py -msgpack==1.1.2 +msgpack==1.2.1 # via cachecontrol mypy==1.20.2 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi mypy-extensions==1.1.0 # via mypy natsort==8.4.0 # via domdf-python-tools numpy==2.2.6 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi packaging==26.2 # via # pytest @@ -179,8 +181,8 @@ packaging==26.2 pathspec==1.1.1 # via mypy pillow==12.2.0 - # via labthings-fastapi (pyproject.toml) -platformdirs==4.9.6 + # via labthings-fastapi +platformdirs==4.10.0 # via apeye pluggy==1.6.0 # via @@ -190,19 +192,19 @@ pycodestyle==2.14.0 # via flake8 pydantic==2.13.4 # via - # labthings-fastapi (pyproject.toml) # fastapi # fastapi-cloud-cli + # labthings-fastapi # pydantic-extra-types # pydantic-settings pydantic-core==2.46.4 # via pydantic pydantic-extra-types==2.11.1 # via fastapi -pydantic-settings==2.14.1 +pydantic-settings==2.14.2 # via fastapi -pydoclint==0.8.4 - # via labthings-fastapi (pyproject.toml) +pydoclint==0.8.7 + # via labthings-fastapi pyflakes==3.4.0 # via flake8 pygments==2.20.0 @@ -213,20 +215,20 @@ pygments==2.20.0 # sphinx # sphinx-prompt # sphinx-tabs -pytest==9.0.3 +pytest==9.1.1 # via - # labthings-fastapi (pyproject.toml) + # labthings-fastapi # pytest-cov # pytest-mock pytest-cov==7.1.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi pytest-mock==3.15.1 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi python-dotenv==1.2.2 # via # pydantic-settings # uvicorn -python-multipart==0.0.29 +python-multipart==0.0.32 # via fastapi pyyaml==6.0.3 # via @@ -249,7 +251,7 @@ rich==15.0.0 # via # rich-toolkit # typer -rich-toolkit==0.19.10 +rich-toolkit==0.20.1 # via # fastapi-cli # fastapi-cloud-cli @@ -263,22 +265,22 @@ rpds-py==0.30.0 # referencing ruamel-yaml==0.19.1 # via sphinx-toolbox -ruff==0.15.14 - # via labthings-fastapi (pyproject.toml) -sentry-sdk==2.60.0 +ruff==0.15.18 + # via labthings-fastapi +sentry-sdk==2.63.0 # via fastapi-cloud-cli shellingham==1.5.4 # via typer six==1.17.0 # via html5lib -snowballstemmer==3.1.0 +snowballstemmer==3.1.1 # via sphinx soupsieve==2.8.4 # via beautifulsoup4 sphinx==8.1.3 # via - # labthings-fastapi (pyproject.toml) # autodocsumm + # labthings-fastapi # sphinx-autoapi # sphinx-autodoc-typehints # sphinx-prompt @@ -287,7 +289,7 @@ sphinx==8.1.3 # sphinx-toolbox # sphinxcontrib-jquery sphinx-autoapi==3.8.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sphinx-autodoc-typehints==3.0.1 # via sphinx-toolbox sphinx-jinja2-compat==0.4.1 @@ -295,11 +297,11 @@ sphinx-jinja2-compat==0.4.1 sphinx-prompt==1.9.0 # via sphinx-toolbox sphinx-rtd-theme==3.1.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sphinx-tabs==3.5.0 # via sphinx-toolbox sphinx-toolbox==4.2.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -315,8 +317,8 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphobjinv==2.4 - # via labthings-fastapi (pyproject.toml) -starlette==1.1.0 + # via labthings-fastapi +starlette==1.3.1 # via fastapi tabulate==0.10.0 # via sphinx-toolbox @@ -324,29 +326,29 @@ tinycss2==1.5.1 # via dict2css tomli==2.4.1 # via - # labthings-fastapi (pyproject.toml) # coverage # fastapi-cli # flake8-pyproject + # labthings-fastapi # mypy # pydoclint # pytest # sphinx -typer==0.26.1 +typer==0.26.7 # via # fastapi-cli # fastapi-cloud-cli types-jsonschema==4.26.0.20260518 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi typing-extensions==4.15.0 # via - # labthings-fastapi (pyproject.toml) # anyio # astroid # beautifulsoup4 # domdf-python-tools # exceptiongroup # fastapi + # labthings-fastapi # mypy # pydantic # pydantic-core @@ -367,7 +369,7 @@ urllib3==2.7.0 # requests # sentry-sdk # sphinx-prompt -uvicorn==0.48.0 +uvicorn==0.49.0 # via # fastapi # fastapi-cli @@ -382,5 +384,5 @@ webencodings==0.5.1 # tinycss2 websockets==16.0 # via uvicorn -zeroconf==0.149.16 - # via labthings-fastapi (pyproject.toml) +zeroconf==0.150.0 + # via labthings-fastapi diff --git a/src/labthings_fastapi/actions.py b/src/labthings_fastapi/actions.py index 06c1e8eb..475f23de 100644 --- a/src/labthings_fastapi/actions.py +++ b/src/labthings_fastapi/actions.py @@ -163,13 +163,13 @@ def id(self) -> uuid.UUID: @property def output(self) -> Any: - """Return value of the Action. If the Action is still running, returns None.""" + """The Action's output (return value) or `None` if it's still running.""" with self._status_lock: return RootModelWrapper.unwrap(self._output_model_instance) @property def output_model_instance(self) -> BaseModel | None: - """Return value of the Action, as a model, or None.""" + """The Action's output (return value), as a model, or None.""" with self._status_lock: return self._output_model_instance diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index fad45929..64762593 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -101,12 +101,12 @@ def __init__(self, obj: Owner | None, cls: type[Owner] | None = None) -> None: @property def owning_class(self) -> type[Owner]: - """Retrieve the class this info object is describing.""" + """The class this info object is describing.""" return self._descriptor_cls @property def owning_object(self) -> Owner | None: - """Retrieve the object to which this info object is bound, if present.""" + """The object to which this info object is bound, if present.""" return self._bound_to_obj @property diff --git a/src/labthings_fastapi/outputs/blob.py b/src/labthings_fastapi/outputs/blob.py index 5229f7e5..cf5233a5 100644 --- a/src/labthings_fastapi/outputs/blob.py +++ b/src/labthings_fastapi/outputs/blob.py @@ -743,7 +743,7 @@ def data(self) -> BlobData: @property def content(self) -> bytes: - """Return the the output as a `bytes` object. + """The blob's content as a `bytes` object in memory. This property may return the `bytes` object, or if we have a file it will read the file and return the contents. Client objects may use diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 2688fe21..16966a35 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -12,7 +12,6 @@ from typing import ( Any, AsyncGenerator, - AsyncIterator, Literal, Optional, TYPE_CHECKING, @@ -20,8 +19,6 @@ overload, ) from typing_extensions import Self -from copy import copy -from contextlib import asynccontextmanager import threading import anyio import logging @@ -203,36 +200,12 @@ async def ringbuffer_entry(self, i: int) -> RingbufferEntry: raise ValueError("the ith frame has been overwritten") return entry - @asynccontextmanager - async def buffer_for_reading(self, i: int) -> AsyncIterator[bytes]: - """Yield the ith frame as a bytes object. - - Retrieve frame ``i`` from the ringbuffer. - - This allows async code access to a frame in the ringbuffer. - The frame will not be copied, and should not be written to. - The frame may not exist after the function has completed (i.e. - 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 reuse). - Currently, buffers are always created as fresh `bytes` objects, so - this context manager does not provide additional functionality - over `.MJPEGStream.ringbuffer_entry`. - - :param i: The index of the frame to read - - :yield: The frame's data as `bytes`, along with timestamp and index. - """ - entry = await self.ringbuffer_entry(i) - yield entry.frame - async def next_frame(self) -> int: """Wait for the next frame, and return its index. This async function will yield until a new frame arrives, then return its index. The index may then be used to retrieve the new frame - with `.MJPEGStream.buffer_for_reading`. + with `.MJPEGStream.ringbuffer_entry`. :return: the index of the next frame to arrive. @@ -253,8 +226,8 @@ async def grab_frame(self) -> bytes: :return: The next JPEG frame, as a `bytes` object. """ i = await self.next_frame() - async with self.buffer_for_reading(i) as frame: - return copy(frame) + entry = await self.ringbuffer_entry(i) + return entry.frame async def next_frame_size(self) -> int: """Wait for the next frame and return its size. @@ -264,20 +237,13 @@ async def next_frame_size(self) -> int: :return: The size of the next JPEG frame, in bytes. """ i = await self.next_frame() - async with self.buffer_for_reading(i) as frame: - return len(frame) + entry = await self.ringbuffer_entry(i) + return len(entry.frame) async def frame_async_generator(self) -> AsyncGenerator[bytes, None]: """Yield frames as bytes objects. - This generator will return frames from the MJPEG stream. These are - taken from the ringbuffer by `.MJPEGStream.buffer_for_reading` and - so should have any buffer-management considerations taken care of. - - Code using this generator should complete as quickly as possible, - because future implementations may hold a lock while this function - yields. If lengthy processing is required, please copy the buffer - and continue processing elsewhere. + This generator will return frames from the MJPEG stream. Note that this will wait for a new frame each time. There is no guarantee that we won't skip frames. @@ -288,8 +254,8 @@ async def frame_async_generator(self) -> AsyncGenerator[bytes, None]: while self._streaming: try: i = await self.next_frame() - async with self.buffer_for_reading(i) as frame: - yield frame + entry = await self.ringbuffer_entry(i) + yield entry.frame except StopAsyncIteration: break except Exception as e: # noqa: BLE001 diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py index d45e3fe2..05afbe29 100644 --- a/src/labthings_fastapi/server/__init__.py +++ b/src/labthings_fastapi/server/__init__.py @@ -291,7 +291,7 @@ def things(self) -> Mapping[str, Thing]: @property def application_config(self) -> Mapping[str, Any] | None: - """Return the application configuration from the config file. + """The application configuration from the config file. :return: The custom configuration as specified in the configuration file. diff --git a/src/labthings_fastapi/testing.py b/src/labthings_fastapi/testing.py index da890973..c95fa3fd 100644 --- a/src/labthings_fastapi/testing.py +++ b/src/labthings_fastapi/testing.py @@ -153,7 +153,7 @@ def _action_manager(self) -> ActionManager: @property def application_config(self) -> None: - """Return an empty application configuration when mocking. + """An empty application configuration when mocking. :return: None """ @@ -161,7 +161,7 @@ def application_config(self) -> None: @property def global_lock(self) -> GlobalLock | None: - """Return a global lock.""" + """A global lock.""" return self._global_lock diff --git a/src/labthings_fastapi/thing.py b/src/labthings_fastapi/thing.py index fb945505..b3a06a93 100644 --- a/src/labthings_fastapi/thing.py +++ b/src/labthings_fastapi/thing.py @@ -338,7 +338,7 @@ def save_settings(self) -> None: @property def thing_state(self) -> Mapping: - """Return a dictionary summarising our current state. + """A dictionary summarising our current state. This is intended to be an easy way to collect metadata from a Thing that summarises its state. It might be used, for example, to record metadata