1212import os
1313import logging
1414
15- from fastapi import FastAPI , Request
15+ from fastapi import APIRouter , FastAPI , Request
1616from fastapi .middleware .cors import CORSMiddleware
1717from anyio .from_thread import BlockingPortal
1818from 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 debug : bool = False ,
7071 ) -> None :
@@ -83,8 +84,9 @@ def __init__(
8384 arguments, and any connections to other `.Thing`\ s.
8485 :param settings_folder: the location on disk where `.Thing`
8586 settings will be saved.
87+ :param api_prefix: An optional prefix for all API routes. This must either
88+ be empty, or start with a slash and not end with a slash.
8689 :param application_config: A mapping containing custom configuration for the
87- application. This is not processed by LabThings. Each `.Thing` can access
8890 application. This is not processed by LabThings. Each `.Thing` can access
8991 this via the Thing-Server interface.
9092 :param debug: If ``True``, set the log level for `.Thing` instances to
@@ -103,9 +105,9 @@ def __init__(
103105 self ._set_url_for_middleware ()
104106 self .settings_folder = settings_folder or "./settings"
105107 self .action_manager = ActionManager ()
106- self .action_manager .attach_to_app ( self .app )
107- self .app .include_router (blob .router ) # include blob download endpoint
108- self ._add_things_view_to_app ( )
108+ self .app . include_router ( self . action_manager .router (), prefix = self ._api_prefix )
109+ self .app .include_router (blob .router , prefix = self . _api_prefix )
110+ self .app . include_router ( self . _things_view_router (), prefix = self . _api_prefix )
109111 self .blocking_portal : Optional [BlockingPortal ] = None
110112 self .startup_status : dict [str , str | dict ] = {"things" : {}}
111113 global _thing_servers # noqa: F824
@@ -171,6 +173,15 @@ def application_config(self) -> Mapping[str, Any] | None:
171173 """
172174 return self ._config .application_config
173175
176+ @property
177+ def _api_prefix (self ) -> str :
178+ """A string that prefixes all URLs in the application.
179+
180+ This must either be empty, or start with a slash and not
181+ end with a slash.
182+ """
183+ return self ._config .api_prefix
184+
174185 ThingInstance = TypeVar ("ThingInstance" , bound = Thing )
175186
176187 def things_by_class (self , cls : type [ThingInstance ]) -> Sequence [ThingInstance ]:
@@ -214,7 +225,7 @@ def path_for_thing(self, name: str) -> str:
214225 """
215226 if name not in self ._things :
216227 raise KeyError (f"No thing named { name } has been added to this server." )
217- return f"/{ name } /"
228+ return f"{ self . _api_prefix } /{ name } /"
218229
219230 def _create_things (self ) -> Mapping [str , Thing ]:
220231 r"""Create the Things, add them to the server, and connect them up if needed.
@@ -322,15 +333,14 @@ async def lifespan(self, app: FastAPI) -> AsyncGenerator[None, None]:
322333
323334 self .blocking_portal = None
324335
325- def _add_things_view_to_app (self ) -> None :
326- """Add an endpoint that shows the list of attached things."""
336+ def _things_view_router (self ) -> APIRouter :
337+ """Create a router for the endpoint that shows the list of attached things.
338+
339+ :returns: an APIRouter with the `thing_descriptions` endpoint.
340+ """
341+ router = APIRouter ()
327342 thing_server = self
328343
329- @self .app .get (
330- "/thing_descriptions/" ,
331- response_model_exclude_none = True ,
332- response_model_by_alias = True ,
333- )
334344 def thing_descriptions (request : Request ) -> Mapping [str , ThingDescription ]:
335345 """Describe all the things available from this server.
336346
@@ -351,6 +361,15 @@ def thing_descriptions(request: Request) -> Mapping[str, ThingDescription]:
351361 for name , thing in thing_server .things .items ()
352362 }
353363
364+ router .add_api_route (
365+ "/thing_descriptions/" ,
366+ thing_descriptions ,
367+ response_model_exclude_none = True ,
368+ response_model_by_alias = True ,
369+ )
370+
371+ return router
372+
354373 @self .app .get ("/things/" )
355374 def thing_paths (request : Request ) -> Mapping [str , str ]:
356375 """URLs pointing to the Thing Descriptions of each Thing.
0 commit comments