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: A 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
@@ -96,16 +98,17 @@ def __init__(
9698 self ._config = ThingServerConfig (
9799 things = things ,
98100 settings_folder = settings_folder ,
101+ api_prefix = api_prefix ,
99102 application_config = application_config ,
100103 )
101104 self .app = FastAPI (lifespan = self .lifespan )
102105 self ._set_cors_middleware ()
103106 self ._set_url_for_middleware ()
104107 self .settings_folder = settings_folder or "./settings"
105108 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 ( )
109+ self .app . include_router ( self . action_manager .router (), prefix = self ._api_prefix )
110+ self .app .include_router (blob .router , prefix = self . _api_prefix )
111+ self .app . include_router ( self . _things_view_router (), prefix = self . _api_prefix )
109112 self .blocking_portal : Optional [BlockingPortal ] = None
110113 self .startup_status : dict [str , str | dict ] = {"things" : {}}
111114 global _thing_servers # noqa: F824
@@ -171,6 +174,15 @@ def application_config(self) -> Mapping[str, Any] | None:
171174 """
172175 return self ._config .application_config
173176
177+ @property
178+ def _api_prefix (self ) -> str :
179+ r"""A string that prefixes all URLs in the application.
180+
181+ This will either be empty, or start with a slash and not
182+ end with a slash. Validation is performed in `.ThingServerConfig`\ .
183+ """
184+ return self ._config .api_prefix
185+
174186 ThingInstance = TypeVar ("ThingInstance" , bound = Thing )
175187
176188 def things_by_class (self , cls : type [ThingInstance ]) -> Sequence [ThingInstance ]:
@@ -214,7 +226,7 @@ def path_for_thing(self, name: str) -> str:
214226 """
215227 if name not in self ._things :
216228 raise KeyError (f"No thing named { name } has been added to this server." )
217- return f"/{ name } /"
229+ return f"{ self . _api_prefix } /{ name } /"
218230
219231 def _create_things (self ) -> Mapping [str , Thing ]:
220232 r"""Create the Things, add them to the server, and connect them up if needed.
@@ -322,11 +334,15 @@ async def lifespan(self, app: FastAPI) -> AsyncGenerator[None, None]:
322334
323335 self .blocking_portal = None
324336
325- def _add_things_view_to_app (self ) -> None :
326- """Add an endpoint that shows the list of attached things."""
337+ def _things_view_router (self ) -> APIRouter :
338+ """Create a router for the endpoint that shows the list of attached things.
339+
340+ :returns: an APIRouter with the `thing_descriptions` endpoint.
341+ """
342+ router = APIRouter ()
327343 thing_server = self
328344
329- @self . app .get (
345+ @router .get (
330346 "/thing_descriptions/" ,
331347 response_model_exclude_none = True ,
332348 response_model_by_alias = True ,
@@ -347,11 +363,13 @@ def thing_descriptions(request: Request) -> Mapping[str, ThingDescription]:
347363 dictionaries.
348364 """
349365 return {
350- name : thing .thing_description (name + "/" , base = str (request .base_url ))
366+ name : thing .thing_description (
367+ path = f"{ self ._api_prefix } /{ name } /" , base = str (request .base_url )
368+ )
351369 for name , thing in thing_server .things .items ()
352370 }
353371
354- @self . app .get ("/things/" )
372+ @router .get ("/things/" )
355373 def thing_paths (request : Request ) -> Mapping [str , str ]:
356374 """URLs pointing to the Thing Descriptions of each Thing.
357375
@@ -361,6 +379,8 @@ def thing_paths(request: Request) -> Mapping[str, str]:
361379 URLs will return the :ref:`wot_td` of one `.Thing` each.
362380 """ # noqa: D403 (URLs is correct capitalisation)
363381 return {
364- t : f" { str (request .base_url ). rstrip ( '/' ) } { t } "
382+ t : str (request .url_for ( f"things. { t } "))
365383 for t in thing_server .things .keys ()
366384 }
385+
386+ return router
0 commit comments