diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index efe4ae5b..08fb8ac4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13"] + python: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v7 @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13"] + python: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v7 diff --git a/README.md b/README.md index e4c59e49..6640a08b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ See [readthedocs] for installation instructions that are automatically tested. Y ### Installation Notes -`labthings-fastapi` supports **Python 3.10, 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current `pydantic-core` dependencies. +`labthings-fastapi` supports **Python 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current `pydantic-core` dependencies. > **Note: Windows Installations on devices with ARM processors** > diff --git a/dev-requirements.txt b/dev-requirements.txt index 39740097..ab9a0920 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -36,9 +36,13 @@ babel==2.18.0 # via sphinx beautifulsoup4==4.15.0 # via sphinx-toolbox +boolean-py==5.0 + # via license-expression cachecontrol==0.14.4 - # via sphinx-toolbox -certifi==2026.7.22 + # via + # pip-audit + # sphinx-toolbox +certifi==2026.10.23 # via # httpcore # httpx @@ -55,13 +59,17 @@ click==8.5.0 # uvicorn codespell==2.4.3 # via labthings-fastapi -coverage==7.16.2 +coverage==7.17.0 # via pytest-cov +cyclonedx-python-lib==11.12.0 + # via pip-audit +defusedxml==0.7.1 + # via py-serializable detect-installer==0.2.1 # via fastapi-cloud-cli dict2css==0.6.0 # via sphinx-toolbox -dnspython==2.8.0 +dnspython==2.9.0 # via email-validator docstring-parser-fork==0.0.16 # via pydoclint @@ -74,7 +82,7 @@ docutils==0.21.2 # sphinx-rtd-theme # sphinx-tabs # sphinx-toolbox -domdf-python-tools==3.10.0 +domdf-python-tools==3.11.0 # via # apeye # apeye-core @@ -84,10 +92,6 @@ email-validator==2.3.0 # via # fastapi # pydantic -exceptiongroup==1.3.1 - # via - # anyio - # pytest fastapi==0.135.4 # via labthings-fastapi fastapi-cli==0.0.32 @@ -96,7 +100,7 @@ fastapi-cloud-cli==0.26.0 # via fastapi-cli fastar==0.12.0 # via fastapi-cloud-cli -filelock==4.0.12 +filelock==4.2.4 # via # cachecontrol # sphinx-toolbox @@ -122,15 +126,15 @@ html5lib==1.1 # via sphinx-toolbox httpcore==1.0.9 # via httpx -httpcore2==2.13.1 +httpcore2==2.16.0 # via httpx2 -httptools==0.8.0 +httptools==0.9.0 # via uvicorn httpx==0.28.1 # via # fastapi # fastapi-cloud-cli -httpx2==2.13.1 +httpx2==2.16.0 # via labthings-fastapi idna==3.20 # via @@ -156,6 +160,7 @@ jinja2==3.1.6 # sphinx # sphinx-autoapi # sphinx-jinja2-compat + # sphinx-prompt jsonschema==4.26.0 # via # labthings-fastapi @@ -164,6 +169,8 @@ jsonschema-specifications==2025.9.1 # via jsonschema librt==0.16.0 # via mypy +license-expression==30.4.4 + # via cyclonedx-python-lib markdown-it-py==3.0.0 # via # mdit-py-plugins @@ -179,7 +186,7 @@ mdit-py-plugins==0.6.1 # via myst-parser mdurl==0.1.2 # via markdown-it-py -msgpack==1.2.3 +msgpack==1.2.4 # via cachecontrol mypy==1.20.2 # via labthings-fastapi @@ -189,38 +196,54 @@ myst-parser==4.0.1 # via labthings-fastapi natsort==8.4.0 # via domdf-python-tools -numpy==2.2.6 +numpy==2.4.6 # via labthings-fastapi +packageurl-python==0.17.6 + # via cyclonedx-python-lib packaging==26.3 # via + # pip-audit + # pip-requirements-parser # pytest # sphinx pathspec==1.1.1 # via mypy -pillow==12.3.0 +pillow==13.0.0 # via labthings-fastapi -platformdirs==4.12.4 - # via apeye -pluggy==1.6.0 +pip==26.2.2 + # via pip-api +pip-api==0.0.35 + # via pip-audit +pip-audit==2.10.1 + # via labthings-fastapi +pip-requirements-parser==32.0.1 + # via pip-audit +platformdirs==4.13.6 + # via + # apeye + # pip-audit +pluggy==1.7.0 # via # pytest # pytest-cov +py-serializable==2.1.0 + # via cyclonedx-python-lib pycodestyle==2.15.0 # via flake8 -pydantic==2.14.0 +pydantic==2.14.1 # via # fastapi # fastapi-cloud-cli # labthings-fastapi # pydantic-extra-types # pydantic-settings -pydantic-core==2.50.0 +pydantic-core==2.50.1 # via pydantic pydantic-extra-types==2.11.1 # via fastapi -pydantic-settings==2.15.0 +pydantic-settings==2.16.1 # via fastapi -pydoclint==0.11.0 +pydoclint==0.11.1 # via labthings-fastapi pyflakes==4.0.3 # via flake8 @@ -232,14 +255,16 @@ pygments==2.21.0 # sphinx # sphinx-prompt # sphinx-tabs -pytest==9.1.1 +pyparsing==3.3.3 + # via pip-requirements-parser +pytest==9.2.0 # via # labthings-fastapi # pytest-cov # pytest-mock pytest-cov==7.1.0 # via labthings-fastapi -pytest-mock==3.16.0 +pytest-mock==3.17.0 # via labthings-fastapi python-dotenv==1.2.4 # via @@ -262,11 +287,14 @@ requests==2.34.2 # via # apeye # cachecontrol + # pip-audit # sphinx + # sphinx-prompt restructuredtext-lint==2.0.2 # via flake8-rst-docstrings rich==15.0.0 # via + # pip-audit # rich-toolkit # typer rich-toolkit==0.20.6 @@ -277,15 +305,19 @@ rignore==0.8.1 # via fastapi-cloud-cli roman==5.2 # via sphinx-toolbox -rpds-py==0.30.0 +roman-numerals==4.1.0 + # via roman-numerals-py +roman-numerals-py==4.1.0 + # via sphinx +rpds-py==2026.9.1 # via # jsonschema # referencing ruamel-yaml==0.19.1 # via sphinx-toolbox -ruff==0.16.10 +ruff==0.17.3 # via labthings-fastapi -sentry-sdk==2.71.0 +sentry-sdk==2.73.0 # via fastapi-cloud-cli shellingham==1.5.4 # via typer @@ -293,9 +325,11 @@ six==1.17.0 # via html5lib snowballstemmer==3.1.1 # via sphinx -soupsieve==2.10 +sortedcontainers==2.4.0 + # via cyclonedx-python-lib +soupsieve==3.0.4 # via beautifulsoup4 -sphinx==8.1.3 +sphinx==8.2.3 # via # autodocsumm # labthings-fastapi @@ -309,11 +343,11 @@ sphinx==8.1.3 # sphinxcontrib-jquery sphinx-autoapi==3.8.1 # via labthings-fastapi -sphinx-autodoc-typehints==3.0.1 +sphinx-autodoc-typehints==3.5.2 # via sphinx-toolbox sphinx-jinja2-compat==0.4.1 # via sphinx-toolbox -sphinx-prompt==1.9.0 +sphinx-prompt==1.10.2 # via sphinx-toolbox sphinx-rtd-theme==3.1.0 # via labthings-fastapi @@ -329,7 +363,7 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-jsmath==2.0.0 # via sphinx sphinxcontrib-qthelp==2.0.0 # via sphinx @@ -337,23 +371,17 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphobjinv==2.4 # via labthings-fastapi -starlette==1.7.0 +starlette==1.10.0 # via fastapi tabulate==0.10.0 # via sphinx-toolbox tinycss2==1.5.1 # via dict2css tomli==2.5.0 - # via - # coverage - # fastapi-cli - # flake8-pyproject - # labthings-fastapi - # mypy - # pydoclint - # pytest - # sphinx -truststore==0.10.4 + # via pip-audit +tomli-w==1.2.0 + # via pip-audit +truststore==0.10.5 # via # httpcore2 # httpx2 @@ -366,10 +394,9 @@ types-jsonschema==4.26.0.20261006 typing-extensions==4.16.0 # via # anyio - # astroid # beautifulsoup4 + # cyclonedx-python-lib # domdf-python-tools - # exceptiongroup # fastapi # httpx2 # labthings-fastapi @@ -377,12 +404,12 @@ typing-extensions==4.16.0 # pydantic # pydantic-core # pydantic-extra-types + # pydantic-settings # referencing # rich-toolkit # sphinx-toolbox # starlette # typing-inspection - # uvicorn typing-inspection==0.4.4 # via # fastapi @@ -406,7 +433,7 @@ webencodings==0.6.1 # via # html5lib # tinycss2 -websockets==16.1.1 +websockets==17.3 # via uvicorn zeroconf==0.151.5 # via labthings-fastapi diff --git a/docs/source/index.rst b/docs/source/index.rst index a4829189..9622f2c4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -52,7 +52,7 @@ Installation ``pip install labthings-fastapi`` -``labthings-fastapi`` supports **Python 3.10, 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. +``labthings-fastapi`` supports **Python 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. .. include:: _windows_arm_note.rst diff --git a/docs/source/testing.md b/docs/source/testing.md index d929edf4..54e664bc 100644 --- a/docs/source/testing.md +++ b/docs/source/testing.md @@ -9,7 +9,7 @@ You can find out more about contributing code in our [Contributor Guidelines]. Here is what the CI pipeline tests: -* **Matrix Testing:** We run the core test suite (`pytest`) and static type checks (`mypy`) across Python versions 3.10, 3.11, 3.12, and 3.13. +* **Matrix Testing:** We run the core test suite (`pytest`) and static type checks (`mypy`) across Python versions 3.11, 3.12, and 3.13. * **Code Quality & Security:** The pipeline runs `ruff` (formatting and linting), `flake8` (docstrings), `codespell` (spelling), and a `pip-audit` to check for dependency vulnerabilities. * **Coverage:** We track code coverage. A report is generated on your PR to show if your changes increased or decreased overall test coverage. * **Dependency Checks:** We run the tests twice: once with pinned dependencies (`dev-requirements.txt`) for reproducibility, and once with unpinned dependencies (`.[dev]`) to catch upstream breakages early. diff --git a/docs/source/tutorial/installing_labthings.rst b/docs/source/tutorial/installing_labthings.rst index c52e4df8..6a0b9d77 100644 --- a/docs/source/tutorial/installing_labthings.rst +++ b/docs/source/tutorial/installing_labthings.rst @@ -29,7 +29,7 @@ It is also possible to install LabThings from source, by cloning the GitHub repo Installation Notes ++++++++++++++++++ -``labthings-fastapi`` supports **Python 3.10, 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. +``labthings-fastapi`` supports **Python 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. .. include:: ../_windows_arm_note.rst diff --git a/pyproject.toml b/pyproject.toml index 1d109b1a..d022113c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,8 @@ authors = [ ] description = "An implementation of LabThings using FastAPI" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -39,6 +38,7 @@ dev = [ "flake8-pyproject", "flake8-rst", "flake8-rst-docstrings", + "pip-audit", "pydoclint[flake8]", "sphinx-rtd-theme", "sphinx>=7.2", @@ -46,7 +46,6 @@ dev = [ "sphinx-toolbox", "sphobjinv", "myst-parser<5", - "tomli; python_version < '3.11'", "codespell", ] diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index c3b5bc18..b24b2dd8 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -33,11 +33,9 @@ from collections.abc import Iterator from itertools import pairwise from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Generic, Mapping, TypeVar, overload +from typing import TYPE_CHECKING, Any, Generic, Mapping, Self, TypeVar, overload from weakref import WeakKeyDictionary, ref -from typing_extensions import Self - from labthings_fastapi.exceptions import ( DescriptorAddedToClassTwiceError, DescriptorNotAddedToClassError, diff --git a/src/labthings_fastapi/client/__init__.py b/src/labthings_fastapi/client/__init__.py index 3cceea85..18cbea8f 100644 --- a/src/labthings_fastapi/client/__init__.py +++ b/src/labthings_fastapi/client/__init__.py @@ -9,12 +9,11 @@ import time from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any, Optional, Self, Union from urllib.parse import urljoin, urlparse import httpx2 from pydantic import BaseModel, TypeAdapter, ValidationError -from typing_extensions import Self # 3.9, 3.10 compatibility from labthings_fastapi.exceptions import ( ClientPropertyError, diff --git a/src/labthings_fastapi/invocation_contexts.py b/src/labthings_fastapi/invocation_contexts.py index 94bf72b9..67a65e16 100644 --- a/src/labthings_fastapi/invocation_contexts.py +++ b/src/labthings_fastapi/invocation_contexts.py @@ -19,12 +19,10 @@ from contextlib import contextmanager from contextvars import ContextVar from threading import Event, Thread -from typing import Any, Callable +from typing import Any, Callable, Self from uuid import UUID, uuid4 from weakref import WeakValueDictionary -from typing_extensions import Self - from labthings_fastapi.exceptions import ( InvocationCancelledError, NoInvocationContextError, diff --git a/src/labthings_fastapi/middleware/url_for.py b/src/labthings_fastapi/middleware/url_for.py index b6702b02..ca4079d7 100644 --- a/src/labthings_fastapi/middleware/url_for.py +++ b/src/labthings_fastapi/middleware/url_for.py @@ -35,14 +35,13 @@ from collections.abc import Awaitable, Callable, Iterator from contextlib import contextmanager from contextvars import ContextVar -from typing import Any +from typing import Any, Self from fastapi import Request, Response from pydantic import GetCoreSchemaHandler from pydantic.networks import AnyUrl from pydantic_core import core_schema from starlette.datastructures import URL -from typing_extensions import Self from labthings_fastapi.exceptions import NoUrlForContextError diff --git a/src/labthings_fastapi/outputs/blob.py b/src/labthings_fastapi/outputs/blob.py index c3f6275e..763b3bfe 100644 --- a/src/labthings_fastapi/outputs/blob.py +++ b/src/labthings_fastapi/outputs/blob.py @@ -95,6 +95,7 @@ def get_image(self) -> MyImageBlob: ClassVar, Literal, Mapping, + Self, ) from weakref import WeakValueDictionary @@ -108,7 +109,6 @@ def get_image(self) -> MyImageBlob: ) from pydantic.json_schema import JsonSchemaValue from pydantic_core import core_schema -from typing_extensions import Self from labthings_fastapi.exceptions import MediaTypeMismatchError from labthings_fastapi.middleware.url_for import url_for diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 39f4d7e2..2c9b0205 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -16,6 +16,7 @@ AsyncGenerator, Literal, Optional, + Self, Union, overload, ) @@ -23,7 +24,6 @@ import anyio from fastapi import FastAPI from fastapi.responses import HTMLResponse, StreamingResponse -from typing_extensions import Self if TYPE_CHECKING: from labthings_fastapi.thing import Thing diff --git a/src/labthings_fastapi/problem_details.py b/src/labthings_fastapi/problem_details.py index 09c9d4ce..67c166e5 100644 --- a/src/labthings_fastapi/problem_details.py +++ b/src/labthings_fastapi/problem_details.py @@ -6,12 +6,11 @@ from functools import wraps from logging import Logger -from typing import Any, Callable, ParamSpec, TypeVar +from typing import Any, Callable, ParamSpec, Self, TypeVar from fastapi import Response from fastapi.responses import JSONResponse from pydantic import BaseModel, ConfigDict -from typing_extensions import Self from labthings_fastapi import exceptions diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py index 2de6fc68..1515b8b2 100644 --- a/src/labthings_fastapi/properties.py +++ b/src/labthings_fastapi/properties.py @@ -57,6 +57,7 @@ class attribute. Documentation is in strings immediately following the Callable, Generic, Literal, + Self, TypeVar, overload, ) @@ -72,7 +73,7 @@ class attribute. Documentation is in strings immediately following the create_model, with_config, ) -from typing_extensions import Self, TypedDict +from typing_extensions import TypedDict from labthings_fastapi.base_descriptor import ( DescriptorInfoCollection, diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py index 5d336198..208a4765 100644 --- a/src/labthings_fastapi/server/__init__.py +++ b/src/labthings_fastapi/server/__init__.py @@ -12,7 +12,7 @@ from collections.abc import Iterator, Mapping, Sequence from contextlib import AsyncExitStack, asynccontextmanager, contextmanager from types import MappingProxyType -from typing import Any, AsyncGenerator, Optional, TypeVar, overload +from typing import Any, AsyncGenerator, Optional, Self, TypeVar, overload import uvicorn from anyio.from_thread import BlockingPortal @@ -22,7 +22,6 @@ from fastapi.testclient import TestClient from pydantic import ValidationError from pydantic_core import PydanticSerializationError -from typing_extensions import Self from labthings_fastapi.actions import ActionManager from labthings_fastapi.exceptions import GlobalLockBusyError diff --git a/src/labthings_fastapi/thing.py b/src/labthings_fastapi/thing.py index 986b6d26..f2de6ddd 100644 --- a/src/labthings_fastapi/thing.py +++ b/src/labthings_fastapi/thing.py @@ -12,13 +12,12 @@ import os from collections.abc import Mapping from json.decoder import JSONDecodeError -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, Self from anyio.to_thread import run_sync from fastapi import Request, WebSocket from fastapi.encoders import jsonable_encoder from pydantic import ValidationError -from typing_extensions import Self from labthings_fastapi.actions import ActionCollection from labthings_fastapi.base_descriptor import OptionallyBoundDescriptor diff --git a/tests/test_mjpeg_stream.py b/tests/test_mjpeg_stream.py index 1aefa2b8..ba05bd86 100644 --- a/tests/test_mjpeg_stream.py +++ b/tests/test_mjpeg_stream.py @@ -55,20 +55,19 @@ def client(): def test_mjpeg_stream(client): """Verify the MJPEG stream contains at least one frame marker. - A limitation of the TestClient is that it can't actually stream. - This means that all of the frames sent by our test Thing will - arrive in a single packet. - - For now, we just check it starts with the frame separator, - but it might be possible in the future to check there are three - images there. + Each time a new frame is sent, the stream actually emits three chunks + of data: the frame separator, the frame itself, and a newline. """ with client.stream("GET", "/telly/stream") as stream: stream.raise_for_status() received = 0 for b in stream.iter_bytes(): received += 1 - assert b.startswith(b"--frame") + if received % 3 == 1: + # We yield three times per frame, with + # the separator, the frame data, and a newline. + # This checks for the frame separator. + assert b.startswith(b"--frame") if __name__ == "__main__":