Skip to content

hololinked.server.http.HTTPServer

Bases: BaseProtocolServer

HTTP(s) server to expose Thing over HTTP protocol. Supports HTTP 1.1.

Use add_thing, or add_property or add_action or add_event methods to add things to the server.

Source code in repo/hololinked/hololinked/server/http/server.py
class HTTPServer(BaseProtocolServer):
    """
    HTTP(s) server to expose `Thing` over HTTP protocol. Supports HTTP 1.1.

    Use `add_thing`, or `add_property` or `add_action` or `add_event` methods to add things to the server.
    """

    address = IPAddress(default="0.0.0.0", doc="IP address")  # type: str
    # SAST(id='hololinked.server.http.HTTPServer.address', description='B104:hardcoded_bind_all_interfaces', tool='bandit')
    """IP address, especially to bind to all interfaces or not"""

    ssl_context = ClassSelector(
        class_=ssl.SSLContext,
        default=None,
        allow_None=True,
    )  # type: ssl.SSLContext | None
    """SSL context to provide encrypted communication"""

    config = ClassSelector(
        class_=RuntimeConfig,
        default=None,
        allow_None=True,
    )  # type: RuntimeConfig
    """Runtime configuration for the HTTP server. See `hololinked.server.http.config.RuntimeConfig` for details"""

    def __init__(
        self,
        *,
        port: int = 8080,
        address: str = "0.0.0.0",  # SAST(id='hololinked.server.http.HTTPServer.__init__.address', description='B104:hardcoded_bind_all_interfaces', tool='bandit')
        things: list[Thing] | None = None,
        # host: Optional[str] = None,
        logger: structlog.stdlib.BoundLogger | None = None,
        ssl_context: ssl.SSLContext | None = None,
        security_schemes: list[Security] | None = None,
        # protocol_version : int = 1, network_interface : str = 'Ethernet',
        allowed_clients: str | Iterable[str] | None = None,
        config: dict[str, Any] | None = None,
        **kwargs,
    ) -> None:
        """
        Initialize the HTTP server.

        Parameters
        ----------
        port: int, default 8080
            the port at which the server should be run
        address: str, default 0.0.0.0
            IP address, use 0.0.0.0 to bind to all interfaces to expose the server to other devices in the network
            and 127.0.0.1 to bind only to localhost
        logger: structlog.stdlib.BoundLogger, optional
            structlog.stdlib.BoundLogger instance
        ssl_context: ssl.SSLContext
            SSL context to provide encrypted communication
        security_schemes: list[Security], optional
            list of security schemes to be used by the server. If None, no security scheme is used.
        allowed_clients: List[str]
            serves request and sets CORS only from these clients, other clients are reject with 403. Unlike pure CORS
            feature, the server resource is not even executed if the client is not an allowed client.
        **kwargs:
            additional keyword arguments for server configuration. Usually:

            - `property_handler`: `BaseHandler` | `PropertyHandler`, optional.
                custom web request handler for property read-write
            - `action_handler`: `BaseHandler` | `ActionHandler`, optional.
                custom web request handler for action
            - `event_handler`: `EventHandler` | `BaseHandler`, optional.
                custom event handler for sending HTTP SSE

            or RuntimeConfig attributes can be passed as keyword arguments.
        """
        default_config: dict[str, Any] = dict(
            cors=global_config.ALLOW_CORS,
            property_handler=kwargs.get("property_handler", PropertyHandler),
            action_handler=kwargs.get("action_handler", ActionHandler),
            event_handler=kwargs.get("event_handler", EventHandler),
            thing_description_handler=kwargs.get("thing_description_handler", ThingDescriptionHandler),
            RW_multiple_properties_handler=kwargs.get("RW_multiple_properties_handler", RWMultiplePropertiesHandler),
            liveness_probe_handler=kwargs.get("liveness_handler", LivenessProbeHandler),
            readiness_probe_handler=kwargs.get("readiness_handler", ReadinessProbeHandler),
            stop_handler=kwargs.get("stop_handler", StopHandler),
            thing_description_service=kwargs.get("thing_description_service", ThingDescriptionService),
            allowed_clients=allowed_clients,
            security_schemes=security_schemes,
        )
        default_config.update(config or dict())
        # need to be extended when more options are added
        super().__init__(
            port=port,
            address=address,
            logger=logger,
            ssl_context=ssl_context,
            config=RuntimeConfig(**default_config),
        )

        self._IP = f"{self.address}:{self.port}"  # TODO, remove this variable later?
        self.config.server_id = self._IP
        if self.logger is None:
            self.logger = structlog.get_logger().bind(component="http-server", host=f"{self.address}:{self.port}")

        self.tornado_instance = None
        self.app = Application(
            handlers=[
                (
                    r"/stop",
                    self.config.stop_handler,
                    dict(config=self.config, logger=self.logger, owner_inst=self),
                ),
                (
                    r"/liveness",
                    self.config.liveness_probe_handler,
                    dict(config=self.config, logger=self.logger, owner_inst=self),
                ),
                (
                    r"/readiness",
                    self.config.readiness_probe_handler,
                    dict(config=self.config, logger=self.logger, owner_inst=self),
                ),
            ]
        )
        self.router = ApplicationRouter(self.app, self)

        self.add_things(*(things or []))

    @classmethod
    def from_params(cls, id: str, params: str | int | dict | list[str] | None) -> Self:
        # docstring already there in base
        if isinstance(params, int):
            params = dict(port=params)
        elif not isinstance(params, dict):
            raise ValueError("HTTP server parameters must be supplied as a dict or just the port as an integer.")
        return cls(**params)

    def welcome_lines(self) -> list[str]:
        # docstring already there in base
        td_path = "/resources/wot-td?ignore_errors=true"
        lines = ["", "📡 HTTP:"]
        for thing in self.things.values():
            lines.append(f"   ➜ Local:   {self.router.get_basepath(use_localhost=True)}/{thing.id}{td_path}")
            lines.append(f"   ➜ Network: {self.router.get_basepath()}/{thing.id}{td_path}")
        return lines

    async def setup(self) -> None:
        """
        Check if all the requirements are met before starting the server, auto invoked by listen().

        Raises
        ------
        ValueError
            if a `Thing` to be served is not bound to an event loop
        """
        # Add only those code here that needs to be redone always before restarting the server.
        # One time creation attributes/activities must be in init

        # Comments are above the relevant lines, not below
        # 1. clear the event loop in case any pending tasks exist, also restarting with same
        # event loop is buggy, so we remove it.
        ioloop.IOLoop.clear_current()
        # 2. sets async loop for a non-possessing thread as well
        self.serving_loop = get_current_async_loop()  # type: asyncio.AbstractEventLoop
        # 3. every thing must already be bound to an eventloop
        for thing in self.things.values():
            if not thing.eventloop:
                raise ValueError(f"You need to expose thing {thing.id} via an EventLoop before trying to serve it")
        # 4. finally also get a reference of the event loop from tornado
        self.tornado_event_loop = ioloop.IOLoop.current()

        self.tornado_instance = TornadoHTTP1Server(self.app, ssl_options=self.ssl_context)  # type: TornadoHTTP1Server

    async def start(self) -> None:
        await self.setup()
        if self.tornado_instance is None:
            raise RuntimeError("tornado server was not created by setup(), cannot start")
        self.tornado_instance.listen(port=self.port, address=self.address)
        self.logger.info(f"started HTTP webserver at {self._IP}, ready to receive requests.")

    def stop(self, attempt_async_stop: bool = True) -> None:
        """
        Stop the HTTP server - unreliable, use `async_stop()` if possible.

        A stop handler at the path `/stop` with POST method is already implemented that invokes this
        method for the clients.

        Parameters
        ----------
        attempt_async_stop: bool, default `True`
            if `True`, attempts to run the `async_stop` method to close all connections gracefully.
        """
        if attempt_async_stop:
            run_callable_somehow(self.async_stop())
            return
        run_callable_somehow(self.shutdown_tornado())

    async def async_stop(self) -> None:
        """
        Stop the HTTP server.

        A stop handler at the path `/stop` with POST method is already implemented
        that invokes this method for the clients.
        """
        if not self.tornado_instance:
            return
        if self.serving_loop.is_running() and self.serving_loop is not asyncio.get_running_loop():
            await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(self.shutdown_tornado(), self.serving_loop))
            return
        await self.shutdown_tornado()

    async def shutdown_tornado(self) -> None:
        """Stop listening and close every open connection."""
        if not self.tornado_instance:  # type gaurd, not a real logic.
            return
        try:
            self.tornado_instance.stop()
            await self.tornado_instance.close_all_connections()
        except Exception as ex:
            self.logger.error(
                "error while stopping tornado server, use stop() method "
                + f"from hololinked.server and do not reuse the port - {ex}"
            )

    def add_property(
        self,
        URL_path: str,
        property: Property | PropertyAffordance,
        http_methods: str | tuple[str, ...] = ("GET", "PUT"),
        handler: type[BaseHandler] = PropertyHandler,
        **kwargs,
    ) -> None:
        """
        Add a property to be accessible by HTTP.

        Parameters
        ----------
        URL_path: str
            URL path to access the property
        property: Property | PropertyAffordance
            Property (object) to be served or its JSON representation
        http_methods: Tuple[str, str, str]
            tuple of http methods to be used for read, write and delete. Use None or omit HTTP method for
            unsupported operations. For example - for readonly property use ('GET', None, None) or ('GET',)
        handler: BaseHandler | PropertyHandler, optional
            custom handler for the property, otherwise the default handler will be used
        kwargs: dict
            additional keyword arguments to be passed to the handler's __init__

        Raises
        ------
        TypeError
            if `property` is not a `Property`/`PropertyAffordance`, or `handler` is not a `BaseHandler` subclass
        ValueError
            if the read, write or delete HTTP method is not one the operation allows
        """
        if not isinstance(property, (Property, PropertyAffordance)):
            raise TypeError(f"property should be of type Property, given type {type(property)}")
        if not issubklass(handler, BaseHandler):
            raise TypeError(f"handler should be subclass of BaseHandler, given type {type(handler)}")
        if isinstance(property, Property):
            affordance = cast(PropertyAffordance, property.to_metadata())
        else:
            affordance = property
        read_http_method = write_http_method = delete_http_method = None
        http_methods = self.router.adapt_http_methods(http_methods)
        if len(http_methods) == 1:
            read_http_method = http_methods[0]
        elif len(http_methods) == 2:
            read_http_method, write_http_method = http_methods
        elif len(http_methods) == 3:
            read_http_method, write_http_method, delete_http_method = http_methods
        if read_http_method != "GET":
            raise ValueError("read method should be GET")
        if write_http_method and write_http_method not in ["POST", "PUT"]:
            raise ValueError("write method should be POST or PUT")
        if delete_http_method and delete_http_method != "DELETE":
            raise ValueError("delete method should be DELETE")
        kwargs["resource"] = affordance
        kwargs["logger"] = self.logger
        kwargs["config"] = self.config
        kwargs["metadata"] = HandlerMetadata(http_methods=http_methods)
        self.router.add_rule(affordance=affordance, URL_path=URL_path, handler=handler, kwargs=kwargs)

    def add_action(
        self,
        URL_path: str,
        action: Action | ActionAffordance,
        http_method: str | tuple[str, ...] | None = "POST",
        handler: type[BaseHandler] = ActionHandler,
        **kwargs,
    ) -> None:
        """
        Add an action to be accessible by HTTP.

        Parameters
        ----------
        URL_path: str
            URL path to access the action
        action: Action | ActionAffordance
            Action (object) to be served or its JSON representation
        http_method: str
            http method to be used for the action
        handler: BaseHandler | ActionHandler, optional
            custom handler for the action
        kwargs: dict
            additional keyword arguments to be passed to the handler's __init__

        Raises
        ------
        TypeError
            if `action` is not an `Action`/`ActionAffordance`, or `handler` is not a `BaseHandler` subclass
        """
        if not isinstance(action, (Action, ActionAffordance)):
            raise TypeError(f"Given action should be of type Action or ActionAffordance, given type {type(action)}")
        if not issubklass(handler, BaseHandler):
            raise TypeError(f"handler should be subclass of BaseHandler, given type {type(handler)}")
        http_methods = self.router.adapt_http_methods(http_method)
        if isinstance(action, Action):
            affordance = cast(ActionAffordance, action.to_metadata())
        else:
            affordance = action
        kwargs["resource"] = affordance
        kwargs["config"] = self.config
        kwargs["logger"] = self.logger
        kwargs["metadata"] = HandlerMetadata(http_methods=http_methods)
        self.router.add_rule(affordance=affordance, URL_path=URL_path, handler=handler, kwargs=kwargs)

    def add_event(
        self,
        URL_path: str,
        event: Event | EventAffordance | PropertyAffordance,
        handler: type[BaseHandler] = EventHandler,
        **kwargs,
    ) -> None:
        """
        Add an event to be accessible by HTTP server; only GET method is supported for events.

        Parameters
        ----------
        URL_path: str
            URL path to access the event
        event: Event | EventAffordance
            Event (object) to be served or its JSON representation
        handler: BaseHandler | EventHandler, optional
            custom handler for the event
        kwargs: dict
            additional keyword arguments to be passed to the handler's __init__

        Raises
        ------
        TypeError
            if `event` is not an `Event`/`EventAffordance`, or `handler` is not a `BaseHandler` subclass
        """
        if not isinstance(event, (Event, EventAffordance)) and (
            not isinstance(event, PropertyAffordance) or not event.observable
        ):
            raise TypeError(f"event should be of type Event or EventAffordance, given type {type(event)}")
        if not issubklass(handler, BaseHandler):
            raise TypeError(f"handler should be subclass of BaseHandler, given type {type(handler)}")
        if isinstance(event, Event):
            affordance = cast(EventAffordance, event.to_metadata())
        else:
            affordance = event
        kwargs["resource"] = affordance
        kwargs["config"] = self.config
        kwargs["logger"] = self.logger
        kwargs["metadata"] = HandlerMetadata(http_methods=("GET",))
        self.router.add_rule(affordance=affordance, URL_path=URL_path, handler=handler, kwargs=kwargs)

    def add_thing(self, thing: Thing) -> None:
        self.router.add_thing(thing)
        super().add_thing(thing)

    def __hash__(self):
        return hash(self._IP)

    def __eq__(self, other):
        if not isinstance(other, HTTPServer):
            return False
        return self._IP == other._IP

    def __str__(self):
        return f"{self.__class__.__name__}(address={self.address}, port={self.port})"

Attributes

address class-attribute instance-attribute

address = IPAddress(default='0.0.0.0', doc='IP address')

IP address, especially to bind to all interfaces or not

port class-attribute instance-attribute

port = Integer(default=9000, bounds=(1, 65535))

The protocol port

things instance-attribute

things: MutableMapping[str, Thing] = TypeConstrainedDict({}, key_type=str, item_type=Thing)

Every served Thing, by id. Sub-things are not served.

Functions

__init__

__init__(*, port: int = 8080, address: str = '0.0.0.0', things: list[Thing] | None = None, logger: BoundLogger | None = None, ssl_context: SSLContext | None = None, security_schemes: list[Security] | None = None, allowed_clients: str | Iterable[str] | None = None, config: dict[str, Any] | None = None, **kwargs) -> None

Initialize the HTTP server.

Parameters:

Name Type Description Default

port

int

the port at which the server should be run

8080

address

str

IP address, use 0.0.0.0 to bind to all interfaces to expose the server to other devices in the network and 127.0.0.1 to bind only to localhost

'0.0.0.0'

logger

BoundLogger | None

structlog.stdlib.BoundLogger instance

None

ssl_context

SSLContext | None

SSL context to provide encrypted communication

None

security_schemes

list[Security] | None

list of security schemes to be used by the server. If None, no security scheme is used.

None

allowed_clients

str | Iterable[str] | None

serves request and sets CORS only from these clients, other clients are reject with 403. Unlike pure CORS feature, the server resource is not even executed if the client is not an allowed client.

None

**kwargs

additional keyword arguments for server configuration. Usually:

  • property_handler: BaseHandler | PropertyHandler, optional. custom web request handler for property read-write
  • action_handler: BaseHandler | ActionHandler, optional. custom web request handler for action
  • event_handler: EventHandler | BaseHandler, optional. custom event handler for sending HTTP SSE

or RuntimeConfig attributes can be passed as keyword arguments.

{}
Source code in repo/hololinked/hololinked/server/http/server.py
def __init__(
    self,
    *,
    port: int = 8080,
    address: str = "0.0.0.0",  # SAST(id='hololinked.server.http.HTTPServer.__init__.address', description='B104:hardcoded_bind_all_interfaces', tool='bandit')
    things: list[Thing] | None = None,
    # host: Optional[str] = None,
    logger: structlog.stdlib.BoundLogger | None = None,
    ssl_context: ssl.SSLContext | None = None,
    security_schemes: list[Security] | None = None,
    # protocol_version : int = 1, network_interface : str = 'Ethernet',
    allowed_clients: str | Iterable[str] | None = None,
    config: dict[str, Any] | None = None,
    **kwargs,
) -> None:
    """
    Initialize the HTTP server.

    Parameters
    ----------
    port: int, default 8080
        the port at which the server should be run
    address: str, default 0.0.0.0
        IP address, use 0.0.0.0 to bind to all interfaces to expose the server to other devices in the network
        and 127.0.0.1 to bind only to localhost
    logger: structlog.stdlib.BoundLogger, optional
        structlog.stdlib.BoundLogger instance
    ssl_context: ssl.SSLContext
        SSL context to provide encrypted communication
    security_schemes: list[Security], optional
        list of security schemes to be used by the server. If None, no security scheme is used.
    allowed_clients: List[str]
        serves request and sets CORS only from these clients, other clients are reject with 403. Unlike pure CORS
        feature, the server resource is not even executed if the client is not an allowed client.
    **kwargs:
        additional keyword arguments for server configuration. Usually:

        - `property_handler`: `BaseHandler` | `PropertyHandler`, optional.
            custom web request handler for property read-write
        - `action_handler`: `BaseHandler` | `ActionHandler`, optional.
            custom web request handler for action
        - `event_handler`: `EventHandler` | `BaseHandler`, optional.
            custom event handler for sending HTTP SSE

        or RuntimeConfig attributes can be passed as keyword arguments.
    """
    default_config: dict[str, Any] = dict(
        cors=global_config.ALLOW_CORS,
        property_handler=kwargs.get("property_handler", PropertyHandler),
        action_handler=kwargs.get("action_handler", ActionHandler),
        event_handler=kwargs.get("event_handler", EventHandler),
        thing_description_handler=kwargs.get("thing_description_handler", ThingDescriptionHandler),
        RW_multiple_properties_handler=kwargs.get("RW_multiple_properties_handler", RWMultiplePropertiesHandler),
        liveness_probe_handler=kwargs.get("liveness_handler", LivenessProbeHandler),
        readiness_probe_handler=kwargs.get("readiness_handler", ReadinessProbeHandler),
        stop_handler=kwargs.get("stop_handler", StopHandler),
        thing_description_service=kwargs.get("thing_description_service", ThingDescriptionService),
        allowed_clients=allowed_clients,
        security_schemes=security_schemes,
    )
    default_config.update(config or dict())
    # need to be extended when more options are added
    super().__init__(
        port=port,
        address=address,
        logger=logger,
        ssl_context=ssl_context,
        config=RuntimeConfig(**default_config),
    )

    self._IP = f"{self.address}:{self.port}"  # TODO, remove this variable later?
    self.config.server_id = self._IP
    if self.logger is None:
        self.logger = structlog.get_logger().bind(component="http-server", host=f"{self.address}:{self.port}")

    self.tornado_instance = None
    self.app = Application(
        handlers=[
            (
                r"/stop",
                self.config.stop_handler,
                dict(config=self.config, logger=self.logger, owner_inst=self),
            ),
            (
                r"/liveness",
                self.config.liveness_probe_handler,
                dict(config=self.config, logger=self.logger, owner_inst=self),
            ),
            (
                r"/readiness",
                self.config.readiness_probe_handler,
                dict(config=self.config, logger=self.logger, owner_inst=self),
            ),
        ]
    )
    self.router = ApplicationRouter(self.app, self)

    self.add_things(*(things or []))

add_thing

add_thing(thing: Thing) -> None
Source code in repo/hololinked/hololinked/server/http/server.py
def add_thing(self, thing: Thing) -> None:
    self.router.add_thing(thing)
    super().add_thing(thing)

add_things

add_things(*things: Thing) -> None

Adds multiple things to be served.

Source code in repo/hololinked/hololinked/core/interfaces/protocol_server.py
def add_things(self, *things: Thing) -> None:
    """Adds multiple things to be served."""
    for thing in things:
        self.add_thing(thing)

add_property

add_property(URL_path: str, property: Property | PropertyAffordance, http_methods: str | tuple[str, ...] = ('GET', 'PUT'), handler: type[BaseHandler] = PropertyHandler, **kwargs) -> None

Add a property to be accessible by HTTP.

Parameters:

Name Type Description Default

URL_path

str

URL path to access the property

required

property

Property | PropertyAffordance

Property (object) to be served or its JSON representation

required

http_methods

str | tuple[str, ...]

tuple of http methods to be used for read, write and delete. Use None or omit HTTP method for unsupported operations. For example - for readonly property use ('GET', None, None) or ('GET',)

('GET', 'PUT')

handler

type[BaseHandler]

custom handler for the property, otherwise the default handler will be used

PropertyHandler

kwargs

additional keyword arguments to be passed to the handler's init

{}

Raises:

Type Description
TypeError

if property is not a Property/PropertyAffordance, or handler is not a BaseHandler subclass

ValueError

if the read, write or delete HTTP method is not one the operation allows

Source code in repo/hololinked/hololinked/server/http/server.py
def add_property(
    self,
    URL_path: str,
    property: Property | PropertyAffordance,
    http_methods: str | tuple[str, ...] = ("GET", "PUT"),
    handler: type[BaseHandler] = PropertyHandler,
    **kwargs,
) -> None:
    """
    Add a property to be accessible by HTTP.

    Parameters
    ----------
    URL_path: str
        URL path to access the property
    property: Property | PropertyAffordance
        Property (object) to be served or its JSON representation
    http_methods: Tuple[str, str, str]
        tuple of http methods to be used for read, write and delete. Use None or omit HTTP method for
        unsupported operations. For example - for readonly property use ('GET', None, None) or ('GET',)
    handler: BaseHandler | PropertyHandler, optional
        custom handler for the property, otherwise the default handler will be used
    kwargs: dict
        additional keyword arguments to be passed to the handler's __init__

    Raises
    ------
    TypeError
        if `property` is not a `Property`/`PropertyAffordance`, or `handler` is not a `BaseHandler` subclass
    ValueError
        if the read, write or delete HTTP method is not one the operation allows
    """
    if not isinstance(property, (Property, PropertyAffordance)):
        raise TypeError(f"property should be of type Property, given type {type(property)}")
    if not issubklass(handler, BaseHandler):
        raise TypeError(f"handler should be subclass of BaseHandler, given type {type(handler)}")
    if isinstance(property, Property):
        affordance = cast(PropertyAffordance, property.to_metadata())
    else:
        affordance = property
    read_http_method = write_http_method = delete_http_method = None
    http_methods = self.router.adapt_http_methods(http_methods)
    if len(http_methods) == 1:
        read_http_method = http_methods[0]
    elif len(http_methods) == 2:
        read_http_method, write_http_method = http_methods
    elif len(http_methods) == 3:
        read_http_method, write_http_method, delete_http_method = http_methods
    if read_http_method != "GET":
        raise ValueError("read method should be GET")
    if write_http_method and write_http_method not in ["POST", "PUT"]:
        raise ValueError("write method should be POST or PUT")
    if delete_http_method and delete_http_method != "DELETE":
        raise ValueError("delete method should be DELETE")
    kwargs["resource"] = affordance
    kwargs["logger"] = self.logger
    kwargs["config"] = self.config
    kwargs["metadata"] = HandlerMetadata(http_methods=http_methods)
    self.router.add_rule(affordance=affordance, URL_path=URL_path, handler=handler, kwargs=kwargs)

add_action

add_action(URL_path: str, action: Action | ActionAffordance, http_method: str | tuple[str, ...] | None = 'POST', handler: type[BaseHandler] = ActionHandler, **kwargs) -> None

Add an action to be accessible by HTTP.

Parameters:

Name Type Description Default

URL_path

str

URL path to access the action

required

action

Action | ActionAffordance

Action (object) to be served or its JSON representation

required

http_method

str | tuple[str, ...] | None

http method to be used for the action

'POST'

handler

type[BaseHandler]

custom handler for the action

ActionHandler

kwargs

additional keyword arguments to be passed to the handler's init

{}

Raises:

Type Description
TypeError

if action is not an Action/ActionAffordance, or handler is not a BaseHandler subclass

Source code in repo/hololinked/hololinked/server/http/server.py
def add_action(
    self,
    URL_path: str,
    action: Action | ActionAffordance,
    http_method: str | tuple[str, ...] | None = "POST",
    handler: type[BaseHandler] = ActionHandler,
    **kwargs,
) -> None:
    """
    Add an action to be accessible by HTTP.

    Parameters
    ----------
    URL_path: str
        URL path to access the action
    action: Action | ActionAffordance
        Action (object) to be served or its JSON representation
    http_method: str
        http method to be used for the action
    handler: BaseHandler | ActionHandler, optional
        custom handler for the action
    kwargs: dict
        additional keyword arguments to be passed to the handler's __init__

    Raises
    ------
    TypeError
        if `action` is not an `Action`/`ActionAffordance`, or `handler` is not a `BaseHandler` subclass
    """
    if not isinstance(action, (Action, ActionAffordance)):
        raise TypeError(f"Given action should be of type Action or ActionAffordance, given type {type(action)}")
    if not issubklass(handler, BaseHandler):
        raise TypeError(f"handler should be subclass of BaseHandler, given type {type(handler)}")
    http_methods = self.router.adapt_http_methods(http_method)
    if isinstance(action, Action):
        affordance = cast(ActionAffordance, action.to_metadata())
    else:
        affordance = action
    kwargs["resource"] = affordance
    kwargs["config"] = self.config
    kwargs["logger"] = self.logger
    kwargs["metadata"] = HandlerMetadata(http_methods=http_methods)
    self.router.add_rule(affordance=affordance, URL_path=URL_path, handler=handler, kwargs=kwargs)

add_event

add_event(URL_path: str, event: Event | EventAffordance | PropertyAffordance, handler: type[BaseHandler] = EventHandler, **kwargs) -> None

Add an event to be accessible by HTTP server; only GET method is supported for events.

Parameters:

Name Type Description Default

URL_path

str

URL path to access the event

required

event

Event | EventAffordance | PropertyAffordance

Event (object) to be served or its JSON representation

required

handler

type[BaseHandler]

custom handler for the event

EventHandler

kwargs

additional keyword arguments to be passed to the handler's init

{}

Raises:

Type Description
TypeError

if event is not an Event/EventAffordance, or handler is not a BaseHandler subclass

Source code in repo/hololinked/hololinked/server/http/server.py
def add_event(
    self,
    URL_path: str,
    event: Event | EventAffordance | PropertyAffordance,
    handler: type[BaseHandler] = EventHandler,
    **kwargs,
) -> None:
    """
    Add an event to be accessible by HTTP server; only GET method is supported for events.

    Parameters
    ----------
    URL_path: str
        URL path to access the event
    event: Event | EventAffordance
        Event (object) to be served or its JSON representation
    handler: BaseHandler | EventHandler, optional
        custom handler for the event
    kwargs: dict
        additional keyword arguments to be passed to the handler's __init__

    Raises
    ------
    TypeError
        if `event` is not an `Event`/`EventAffordance`, or `handler` is not a `BaseHandler` subclass
    """
    if not isinstance(event, (Event, EventAffordance)) and (
        not isinstance(event, PropertyAffordance) or not event.observable
    ):
        raise TypeError(f"event should be of type Event or EventAffordance, given type {type(event)}")
    if not issubklass(handler, BaseHandler):
        raise TypeError(f"handler should be subclass of BaseHandler, given type {type(handler)}")
    if isinstance(event, Event):
        affordance = cast(EventAffordance, event.to_metadata())
    else:
        affordance = event
    kwargs["resource"] = affordance
    kwargs["config"] = self.config
    kwargs["logger"] = self.logger
    kwargs["metadata"] = HandlerMetadata(http_methods=("GET",))
    self.router.add_rule(affordance=affordance, URL_path=URL_path, handler=handler, kwargs=kwargs)

setup async

setup() -> None

Check if all the requirements are met before starting the server, auto invoked by listen().

Raises:

Type Description
ValueError

if a Thing to be served is not bound to an event loop

Source code in repo/hololinked/hololinked/server/http/server.py
async def setup(self) -> None:
    """
    Check if all the requirements are met before starting the server, auto invoked by listen().

    Raises
    ------
    ValueError
        if a `Thing` to be served is not bound to an event loop
    """
    # Add only those code here that needs to be redone always before restarting the server.
    # One time creation attributes/activities must be in init

    # Comments are above the relevant lines, not below
    # 1. clear the event loop in case any pending tasks exist, also restarting with same
    # event loop is buggy, so we remove it.
    ioloop.IOLoop.clear_current()
    # 2. sets async loop for a non-possessing thread as well
    self.serving_loop = get_current_async_loop()  # type: asyncio.AbstractEventLoop
    # 3. every thing must already be bound to an eventloop
    for thing in self.things.values():
        if not thing.eventloop:
            raise ValueError(f"You need to expose thing {thing.id} via an EventLoop before trying to serve it")
    # 4. finally also get a reference of the event loop from tornado
    self.tornado_event_loop = ioloop.IOLoop.current()

    self.tornado_instance = TornadoHTTP1Server(self.app, ssl_options=self.ssl_context)  # type: TornadoHTTP1Server

start async

start() -> None
Source code in repo/hololinked/hololinked/server/http/server.py
async def start(self) -> None:
    await self.setup()
    if self.tornado_instance is None:
        raise RuntimeError("tornado server was not created by setup(), cannot start")
    self.tornado_instance.listen(port=self.port, address=self.address)
    self.logger.info(f"started HTTP webserver at {self._IP}, ready to receive requests.")

run

run(forked: bool = False, print_welcome_message: bool = True) -> None

Run the server and serve your things.

Use this method if this is the only running protocol. Blocks.

Parameters:

Name Type Description Default

forked

bool

whether to run in a forked thread

False

print_welcome_message

bool

whether to print a welcome message on startup, like the ports and access points

True
Source code in repo/hololinked/hololinked/core/interfaces/protocol_server.py
@forkable
def run(self, forked: bool = False, print_welcome_message: bool = True) -> None:
    """
    Run the server and serve your things.

    Use this method if this is the only running protocol. Blocks.

    Parameters
    ----------
    forked: bool, default False
        whether to run in a forked thread
    print_welcome_message: bool, default True
        whether to print a welcome message on startup, like the ports and access points
    """
    from hololinked.server import run

    run(self, print_welcome_message=print_welcome_message)

stop

stop(attempt_async_stop: bool = True) -> None

Stop the HTTP server - unreliable, use async_stop() if possible.

A stop handler at the path /stop with POST method is already implemented that invokes this method for the clients.

Parameters:

Name Type Description Default

attempt_async_stop

bool

if True, attempts to run the async_stop method to close all connections gracefully.

True
Source code in repo/hololinked/hololinked/server/http/server.py
def stop(self, attempt_async_stop: bool = True) -> None:
    """
    Stop the HTTP server - unreliable, use `async_stop()` if possible.

    A stop handler at the path `/stop` with POST method is already implemented that invokes this
    method for the clients.

    Parameters
    ----------
    attempt_async_stop: bool, default `True`
        if `True`, attempts to run the `async_stop` method to close all connections gracefully.
    """
    if attempt_async_stop:
        run_callable_somehow(self.async_stop())
        return
    run_callable_somehow(self.shutdown_tornado())

async_stop async

async_stop() -> None

Stop the HTTP server.

A stop handler at the path /stop with POST method is already implemented that invokes this method for the clients.

Source code in repo/hololinked/hololinked/server/http/server.py
async def async_stop(self) -> None:
    """
    Stop the HTTP server.

    A stop handler at the path `/stop` with POST method is already implemented
    that invokes this method for the clients.
    """
    if not self.tornado_instance:
        return
    if self.serving_loop.is_running() and self.serving_loop is not asyncio.get_running_loop():
        await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(self.shutdown_tornado(), self.serving_loop))
        return
    await self.shutdown_tornado()

shutdown_tornado async

shutdown_tornado() -> None

Stop listening and close every open connection.

Source code in repo/hololinked/hololinked/server/http/server.py
async def shutdown_tornado(self) -> None:
    """Stop listening and close every open connection."""
    if not self.tornado_instance:  # type gaurd, not a real logic.
        return
    try:
        self.tornado_instance.stop()
        await self.tornado_instance.close_all_connections()
    except Exception as ex:
        self.logger.error(
            "error while stopping tornado server, use stop() method "
            + f"from hololinked.server and do not reuse the port - {ex}"
        )