Skip to content

Commit

Permalink
Fix thing description to include base URL
Browse files Browse the repository at this point in the history
  • Loading branch information
rwb27 committed Nov 6, 2023
1 parent 08a4a2b commit c1da4de
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 17 deletions.
34 changes: 24 additions & 10 deletions src/labthings_fastapi/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import TYPE_CHECKING, Optional
from collections.abc import Mapping
from fastapi.encoders import jsonable_encoder
from fastapi import Request
from anyio.abc import ObjectSendStream
from anyio.from_thread import BlockingPortal
from anyio.to_thread import run_sync
Expand Down Expand Up @@ -97,10 +98,12 @@ def attach_to_server(self, server: ThingServer, path: str):
@server.app.get(
self.path,
summary=get_summary(self.thing_description),
description=get_docstring(self.thing_description)
description=get_docstring(self.thing_description),
response_model_exclude_none=True,
response_model_by_alias=True,
)
def thing_description():
return self.thing_description_dict()
def thing_description(request: Request) -> ThingDescription:
return self.thing_description(base=str(request.base_url))

@server.app.websocket(self.path + "ws")
async def websocket(ws: WebSocket):
Expand Down Expand Up @@ -144,8 +147,8 @@ def validate_thing_description(self):
td = self.thing_description_dict()
return validation.validate_thing_description(td)

_cached_thing_description: Optional[tuple[str, ThingDescription]] = None
def thing_description(self, path: Optional[str] = None) -> ThingDescription:
_cached_thing_description: Optional[tuple[str, str, ThingDescription]] = None
def thing_description(self, path: Optional[str] = None, base: Optional[str] = None) -> ThingDescription:
"""A w3c Thing Description representing this thing
The w3c Web of Things working group defined a standard representation
Expand All @@ -154,8 +157,12 @@ def thing_description(self, path: Optional[str] = None) -> ThingDescription:
representation of the Thing Description for this Thing.
"""
path = path or getattr(self, "path", "{base_uri}")
if self._cached_thing_description and self._cached_thing_description[0] == path:
return self._cached_thing_description[1]
if (
self._cached_thing_description
and self._cached_thing_description[0] == path
and self._cached_thing_description[1] == base
):
return self._cached_thing_description[2]

properties = {}
actions = {}
Expand All @@ -165,23 +172,30 @@ def thing_description(self, path: Optional[str] = None) -> ThingDescription:
if hasattr(item, "action_affordance"):
actions[name] = (item.action_affordance(self, path))

return ThingDescription(
td = ThingDescription(
title=getattr(self, "title", self.__class__.__name__),
properties=properties,
actions=actions,
security="no_security",
securityDefinitions={"no_security": NoSecurityScheme()},
base=base,
)
self._cached_thing_description = (path, base, td)
return td

def thing_description_dict(self, path: Optional[str] = None) -> dict:
def thing_description_dict(
self,
path: Optional[str] = None,
base: Optional[str] = None,
) -> dict:
"""A w3c Thing Description representing this thing, as a simple dict
The w3c Web of Things working group defined a standard representation
of a Thing, which provides a high-level description of the actions,
properties, and events that it exposes. This endpoint delivers a JSON
representation of the Thing Description for this Thing.
"""
td: ThingDescription = self.thing_description(path=path)
td: ThingDescription = self.thing_description(path=path, base=base)
td_dict: dict = td.model_dump(exclude_none=True, by_alias=True)
return jsonable_encoder(td_dict)

Expand Down
22 changes: 15 additions & 7 deletions src/labthings_fastapi/thing_server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations
from typing import Optional, Sequence, TypeVar
import os.path
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from anyio.from_thread import BlockingPortal
from contextlib import asynccontextmanager, AsyncExitStack
Expand All @@ -11,6 +11,7 @@
from .actions import ActionManager
from .thing_settings import ThingSettings
from .thing import Thing
from .thing_description.model import ThingDescription


_thing_servers: WeakSet[ThingServer] = WeakSet()
Expand Down Expand Up @@ -120,14 +121,21 @@ async def lifespan(self, app: FastAPI):
def add_things_view_to_app(self):
"""Add an endpoint that shows the list of attached things."""
thing_server = self
@self.app.get("/thing_descriptions/")
def thing_descriptions() -> Mapping[str, Mapping]:
@self.app.get(
"/thing_descriptions/",
response_model_exclude_none=True,
response_model_by_alias=True,
)
def thing_descriptions(request: Request) -> Mapping[str, ThingDescription]:
"""A dictionary of all the things available from this server"""
return {
path: thing.thing_description(path)
path: thing.thing_description(path, base=str(request.base_url))
for path, thing in thing_server.things.items()
}
@self.app.get("/things/")
def thing_paths() -> Sequence[str]:
"""A dictionary of all the things available from this server"""
return list(thing_server.things.keys())
def thing_paths(request: Request) -> Mapping[str, str]:
"""A list of URLs pointing to the Thing Descriptions of each Thing on the server."""
return {
t: f"{str(request.base_url).rstrip('/')}{t}"
for t in thing_server.things.keys()
}

0 comments on commit c1da4de

Please sign in to comment.