1111from typing_extensions import Self
1212import os
1313
14- from fastapi import FastAPI , Request
14+ from fastapi import APIRouter , FastAPI , Request
1515from fastapi .middleware .cors import CORSMiddleware
1616from anyio .from_thread import BlockingPortal
1717from contextlib import asynccontextmanager , AsyncExitStack
@@ -65,6 +65,7 @@ def __init__(
6565 self ,
6666 things : ThingsConfig ,
6767 settings_folder : Optional [str ] = None ,
68+ api_prefix : str = "" ,
6869 application_config : Optional [Mapping [str , Any ]] = None ,
6970 ) -> None :
7071 r"""Initialise a LabThings server.
@@ -82,8 +83,9 @@ def __init__(
8283 arguments, and any connections to other `.Thing`\ s.
8384 :param settings_folder: the location on disk where `.Thing`
8485 settings will be saved.
86+ :param api_prefix: An optional prefix for all API routes. This must either
87+ be empty, or start with a slash and not end with a slash.
8588 :param application_config: A mapping containing custom configuration for the
86- application. This is not processed by LabThings. Each `.Thing` can access
8789 application. This is not processed by LabThings. Each `.Thing` can access
8890 this via the Thing-Server interface.
8991 """
@@ -99,9 +101,9 @@ def __init__(
99101 self ._set_url_for_middleware ()
100102 self .settings_folder = settings_folder or "./settings"
101103 self .action_manager = ActionManager ()
102- self .action_manager .attach_to_app ( self .app )
103- self .app .include_router (blob .router ) # include blob download endpoint
104- self ._add_things_view_to_app ( )
104+ self .app . include_router ( self . action_manager .router (), prefix = self ._api_prefix )
105+ self .app .include_router (blob .router , prefix = self . _api_prefix )
106+ self .app . include_router ( self . _things_view_router (), prefix = self . _api_prefix )
105107 self .blocking_portal : Optional [BlockingPortal ] = None
106108 self .startup_status : dict [str , str | dict ] = {"things" : {}}
107109 global _thing_servers # noqa: F824
@@ -166,6 +168,15 @@ def application_config(self) -> Mapping[str, Any] | None:
166168 """
167169 return self ._config .application_config
168170
171+ @property
172+ def _api_prefix (self ) -> str :
173+ """A string that prefixes all URLs in the application.
174+
175+ This must either be empty, or start with a slash and not
176+ end with a slash.
177+ """
178+ return self ._config .api_prefix
179+
169180 ThingInstance = TypeVar ("ThingInstance" , bound = Thing )
170181
171182 def things_by_class (self , cls : type [ThingInstance ]) -> Sequence [ThingInstance ]:
@@ -209,7 +220,7 @@ def path_for_thing(self, name: str) -> str:
209220 """
210221 if name not in self ._things :
211222 raise KeyError (f"No thing named { name } has been added to this server." )
212- return f"/{ name } /"
223+ return f"{ self . _api_prefix } /{ name } /"
213224
214225 def _create_things (self ) -> Mapping [str , Thing ]:
215226 r"""Create the Things, add them to the server, and connect them up if needed.
@@ -317,15 +328,14 @@ async def lifespan(self, app: FastAPI) -> AsyncGenerator[None, None]:
317328
318329 self .blocking_portal = None
319330
320- def _add_things_view_to_app (self ) -> None :
321- """Add an endpoint that shows the list of attached things."""
331+ def _things_view_router (self ) -> APIRouter :
332+ """Create a router for the endpoint that shows the list of attached things.
333+
334+ :returns: an APIRouter with the `thing_descriptions` endpoint.
335+ """
336+ router = APIRouter ()
322337 thing_server = self
323338
324- @self .app .get (
325- "/thing_descriptions/" ,
326- response_model_exclude_none = True ,
327- response_model_by_alias = True ,
328- )
329339 def thing_descriptions (request : Request ) -> Mapping [str , ThingDescription ]:
330340 """Describe all the things available from this server.
331341
@@ -346,6 +356,15 @@ def thing_descriptions(request: Request) -> Mapping[str, ThingDescription]:
346356 for name , thing in thing_server .things .items ()
347357 }
348358
359+ router .add_api_route (
360+ "/thing_descriptions/" ,
361+ thing_descriptions ,
362+ response_model_exclude_none = True ,
363+ response_model_by_alias = True ,
364+ )
365+
366+ return router
367+
349368 @self .app .get ("/things/" )
350369 def thing_paths (request : Request ) -> Mapping [str , str ]:
351370 """URLs pointing to the Thing Descriptions of each Thing.
0 commit comments