labthings_fastapi.server ======================== .. py:module:: labthings_fastapi.server .. autoapi-nested-parse:: Code supporting the LabThings server. LabThings wraps the `fastapi.FastAPI` application in a `~lt.ThingServer`, which provides the tools to serve and manage `~lt.Thing` instances. See the :ref:`tutorial` for examples of how to set up a `~lt.ThingServer`. Submodules ---------- .. toctree:: :maxdepth: 1 /autoapi/labthings_fastapi/server/cli/index /autoapi/labthings_fastapi/server/config_model/index /autoapi/labthings_fastapi/server/fallback/index Classes ------- .. autoapisummary:: labthings_fastapi.server.ThingServer Package Contents ---------------- .. py:class:: ThingServer(config: config_model.ThingServerConfig, *, debug: bool = False) ThingServer(*, debug: bool = False, **kwargs: Any) Use FastAPI to serve `~lt.Thing` instances. The `~lt.ThingServer` sets up a `fastapi.FastAPI` application and uses it to expose the capabilities of `~lt.Thing` instances over HTTP. There are several functions of a `~lt.ThingServer`: * Manage where settings are stored, to allow `~lt.Thing` instances to load and save their settings from disk. * Configure the server to allow cross-origin requests (required if we use a web app that is not served from the `~lt.ThingServer`). * Manage the threads used to run :ref:`actions`. * Manage :ref:`blobs` to allow binary data to be returned. * Allow threaded code to call functions in the event loop, by providing an `anyio.from_thread.BlockingPortal`. Initialise a LabThings server. The `~lt.ThingServer` is responsible for running the code in `~lt.Thing` instances, and making them available over the network. It should be configured by passing a `~lt.ThingServerConfig` object (or a dictionary that can be validated as a `~lt.ThingServerConfig` object). For convenience and backwards compatibility, if `config` is `None` the keyword arguments will be passed to `~lt.ThingServerConfig` instead. Keyword arguments may not be used if the `config` argument is used, and may be removed in the future. Setting up the `~lt.ThingServer` involves creating the underlying `fastapi.FastAPI` app, setting its lifespan function (used to set up and shut down the `~lt.Thing` instances), and configuring it to allow cross-origin requests. :param config: a `~lt.ThingServerConfig` object that configures the server, or something that may be converted to one. :param debug: ff ``True``, set the log level for `~lt.Thing` instances to DEBUG. :param \**kwargs: ff keyword arguments are supplied, they will be passed to the constructor of `~lt.ThingServerConfig`\ . This is not allowed if `config` is a `~lt.ThingServerConfig` object. :raises TypeError: if the value of `config` cannot be parsed as a `~lt.ThingServerConfig`\ . :raises ValueError: if keyword arguments are supplied together with `config`\ . .. py:attribute:: startup_failure :type: dict | None :value: None .. py:attribute:: _debug :value: False .. py:attribute:: app .. py:attribute:: action_manager .. py:attribute:: message_broker .. py:attribute:: blocking_portal :type: Optional[anyio.from_thread.BlockingPortal] :value: None .. py:attribute:: startup_status :type: dict[str, str | dict] .. py:attribute:: global_lock .. py:attribute:: _things .. py:method:: from_things(things: config_model.ThingsConfig, debug: bool = False, **kwargs: Any) -> Self :classmethod: Create a ThingServer using a dictionary of `~lt.Thing` subclasses. In test and example code, it's convenient to be able to pass server and `Thing` configurations as keyword arguments rather than a config model. This convenience method will turn its keyword arguments into a server configuration and create a server based on it. :param things: A mapping of names to `Thing` configurations. These may be specified as a `~lt.ThingConfig` object, a `~lt.Thing` subclass, or an import string referencing a `~lt.Thing` subclass. :param debug: Whether to start the server in debug mode. :param \**kwargs: Additional keyword arguments are passed to `~lt.ThingServerConfig`\ . :return: a `~lt.ThingServer` instance. .. py:method:: from_config(config: config_model.ThingServerConfig, debug: bool = False) -> Self :classmethod: Create a ThingServer from a configuration model. This is equivalent to ``ThingServer(config, debug=debug)``\ . :param config: The configuration parameters for the server. :param debug: If ``True``, set the log level for `~lt.Thing` instances to DEBUG. :return: A `~lt.ThingServer` configured as per the model. .. py:method:: _set_cors_middleware() -> None Configure the server to allow requests from other origins. This is required to allow web applications access to the HTTP API, if they are not served from the same origin (i.e. if they are not served as part of the `~lt.ThingServer`.). This is usually needed during development, and may be needed at other times depending on how you are using LabThings. .. py:method:: _set_url_for_middleware() -> None Add middleware to support `url_for` in Pydantic models. This middleware adds a request state variable that allows `labthings_fastapi.server.URLFor` instances to be serialised using FastAPI's `url_for` function. .. py:method:: _add_exception_handlers() -> None Add exception handlers to the FastAPI application. .. py:property:: debug :type: bool Whether the server is in debug mode. .. py:property:: settings_folder :type: str The folder in which we will store `Thing` settings. :raises RuntimeError: if there is no settings folder set. This should never happen, as it's set in `__init__`. .. py:property:: things :type: collections.abc.Mapping[str, labthings_fastapi.thing.Thing] A read-only mapping of names to `~lt.Thing` instances. .. py:property:: application_config :type: collections.abc.Mapping[str, Any] | None Return the application configuration from the config file. :return: The custom configuration as specified in the configuration file. .. py:property:: api_prefix :type: str A string that prefixes all URLs in the application. This will either be empty, or start with a slash and not end with a slash. Validation is performed in `~lt.ThingServerConfig`\ . .. py:attribute:: ThingInstance .. py:method:: things_by_class(cls: type[ThingInstance]) -> collections.abc.Sequence[ThingInstance] Return all Things attached to this server matching a class. Return all instances of ``cls`` attached to this server. :param cls: A `~lt.Thing` subclass. :return: all instances of ``cls`` that have been added to this server. .. py:method:: thing_by_class(cls: type[ThingInstance]) -> ThingInstance Return the instance of ``cls`` attached to this server. This function calls `.ThingServer.things_by_class`, but asserts that there is exactly one match. :param cls: a `~lt.Thing` subclass. :return: the instance of ``cls`` attached to this server. :raise RuntimeError: if there is not exactly one matching Thing. .. py:method:: path_for_thing(name: str) -> str Return the path for a thing with the given name. :param name: The name of the thing. :return: The path at which the thing is served. :raise KeyError: if no thing with the given name has been added. .. py:method:: _create_things() -> collections.abc.Mapping[str, labthings_fastapi.thing.Thing] Create the Things, add them to the server, and connect them up if needed. This method is responsible for creating instances of `~lt.Thing` subclasses and adding them to the server. It also ensures the `~lt.Thing`\ s are connected together if required. The Things are defined in ``self._config.thing_configs`` which in turn is generated from the ``things`` argument to ``__init__``\ . :return: A mapping of names to `~lt.Thing` instances. :raise TypeError: if ``cls`` is not a subclass of `~lt.Thing`. .. py:method:: _connect_things() -> None Connect the `thing_slot` attributes of Things. A `~lt.Thing` may have attributes defined as ``lt.thing_slot()``, which will be populated after all `~lt.Thing` instances are loaded on the server. This function is responsible for supplying the `~lt.Thing` instances required for each connection. This will be done by using the name specified either in the connection's default, or in the configuration of the server. `.ThingSlotError` will be raised by code called by this method if the connection cannot be provided. See `.ThingSlot.connect` for more details. .. py:method:: _attach_things_to_server() -> None Add the Things to the FastAPI App. This calls `~lt.Thing.attach_to_server` on each `~lt.Thing` that is a part of this `~lt.ThingServer` in order to add the HTTP endpoints and load settings. .. py:method:: lifespan(app: fastapi.FastAPI) -> AsyncGenerator[None, None] :async: Manage set up and tear down of the server and Things. This method is used as a lifespan function for the FastAPI app. See the lifespan_ page in FastAPI's documentation. .. _lifespan: https://fastapi.tiangolo.com/advanced/events/#lifespan-function This does two important things: * It sets up the blocking portal so background threads can run async code (this is required for events, streams, etc.). * It runs setup/teardown code for Things by calling them as context managers. :param app: The FastAPI application wrapped by the server. :yield: no value. The FastAPI application will serve requests while this function yields. :raises BaseException: Reraises any errors that are caught when calling ``__enter__`` on each Thing. The error is also saved to ``self.startup_failure`` for post mortem, as otherwise uvicorn will swallow it and replace it with SystemExit(3) and no traceback. .. py:method:: _things_view_router() -> fastapi.APIRouter Create a router for the endpoint that shows the list of attached things. :returns: an APIRouter with the `thing_descriptions` endpoint. .. py:method:: serve(host: str = 'localhost', port: int = 5000) -> None Run the server in `uvicorn`\ . This method will run the server from Python, using `uvicorn.run`\ . This is the most convenient way to run a LabThings server from Python, and is identical to what happens when it is run from the command line. :param host: The IP address or hostname on which to serve. By default, this is ``localhost`` which is only accessible from your computer. To serve over a network on all available IPv4 addresses, use ``"0.0.0.0"``. :param port: The port on which to serve. This defaults to 5000. .. py:method:: test_client() -> collections.abc.Iterator[fastapi.testclient.TestClient] A context manager to test out a server without binding to a port. This context manager will start up the server and run an event loop, but instead of responding to requests on a network port, it uses `fastapi.testclient.TestClient` to simulate HTTP requests. This is provided to simplify test code, and should not be used in production. :yields: a `fastapi.testclient.TestClient` to simulate HTTP requests. .. warning:: Usually, a server is only started up and shut down once. Calling this method multiple times may have unexpected results. As a rule, only ever use this method in your test suite.