Skip to content

hololinked.protocols.http.HTTPServer

Bases: Parameterized

HTTP(s) server to route requests to Thing.

Source code in hololinked\protocols\http\server\__init__.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
class HTTPServer(Parameterized):
    """
    HTTP(s) server to route requests to `Thing`.
    """

    things = TypedList(item_type=str, default=None, allow_None=True, 
                       doc="instance name of the things to be served by the HTTP server." ) # type: typing.List[str]
    port = Integer(default=8080, bounds=(1, 65535),  
                    doc="the port at which the server should be run" ) # type: int
    address = IPAddress(default='0.0.0.0', 
                    doc="IP address") # type: str
    # protocol_version = Selector(objects=[1, 1.1, 2], default=2, 
    #                 doc="for HTTP 2, SSL is mandatory. HTTP2 is recommended. \
    #                 When no SSL configurations are provided, defaults to 1.1" ) # type: float
    logger = ClassSelector(class_=logging.Logger, default=None, allow_None=True, 
                    doc="logging.Logger" ) # type: logging.Logger
    log_level = Selector(objects=[logging.DEBUG, logging.INFO, logging.ERROR, logging.WARN, 
                                logging.CRITICAL, logging.ERROR], 
                    default=logging.INFO, 
                    doc="""alternative to logger, this creates an internal logger with the specified log level 
                    along with a IO stream handler.""" ) # type: int
    serializer = ClassSelector(class_=JSONSerializer,  default=None, allow_None=True,
                    doc="""json serializer used by the server""" ) # type: JSONSerializer
    ssl_context = ClassSelector(class_=ssl.SSLContext, default=None, allow_None=True, 
                    doc="SSL context to provide encrypted communication") # type: typing.Optional[ssl.SSLContext]    
    certfile = String(default=None, allow_None=True, 
                    doc="""alternative to SSL context, provide certificate file & key file to allow the server to 
                        create a SSL context""") # type: str
    keyfile = String(default=None, allow_None=True, 
                    doc="""alternative to SSL context, provide certificate file & key file to allow the server to 
                        create a SSL context""") # type: str
    allowed_clients = TypedList(item_type=str,
                            doc="""Serves request and sets CORS only from these clients, other clients are rejected with 403. 
                                Unlike pure CORS, the server resource is not even executed if the client is not 
                                an allowed client. if None any client is served.""")
    host = String(default=None, allow_None=True, 
                doc="Host Server to subscribe to coordinate starting sequence of remote objects & web GUI" ) # type: str
    # network_interface = String(default='Ethernet',  
    #                         doc="Currently there is no logic to detect the IP addresss (as externally visible) correctly, \
    #                         therefore please send the network interface name to retrieve the IP. If a DNS server is present, \
    #                         you may leave this field" ) # type: str
    property_handler = ClassSelector(default=PropertyHandler, class_=(PropertyHandler, BaseHandler), isinstance=False, 
                            doc="custom web request handler of your choice for property read-write & action execution" ) # type: typing.Union[BaseHandler, PropertyHandler]
    action_handler = ClassSelector(default=ActionHandler, class_=(ActionHandler, BaseHandler), isinstance=False, 
                            doc="custom web request handler of your choice for property read-write & action execution" ) # type: typing.Union[BaseHandler, ActionHandler]
    event_handler = ClassSelector(default=EventHandler, class_=(EventHandler, BaseHandler), isinstance=False, 
                            doc="custom event handler of your choice for handling events") # type: typing.Union[BaseHandler, EventHandler]
    schema_validator = ClassSelector(class_=BaseSchemaValidator, default=JSONSchemaValidator, allow_None=True, isinstance=False,
                        doc="""Validator for JSON schema. If not supplied, a default JSON schema validator is created.""") # type: BaseSchemaValidator



    def __init__(self, 
                things : typing.List[str] | typing.List[Thing] | typing.List[ThingMeta] | None = None, *, 
                port : int = 8080, address : str = '0.0.0.0', host : typing.Optional[str] = None, 
                logger : typing.Optional[logging.Logger] = None, log_level : int = logging.INFO, 
                serializer : typing.Optional[JSONSerializer] = None, ssl_context : typing.Optional[ssl.SSLContext] = None, 
                schema_validator : typing.Optional[BaseSchemaValidator] = JSONSchemaValidator,
                certfile : str = None, keyfile : str = None, 
                # protocol_version : int = 1, network_interface : str = 'Ethernet', 
                allowed_clients : typing.Optional[typing.Union[str, typing.Iterable[str]]] = None,   
                **kwargs
            ) -> None:
        """
        Parameters
        ----------
        things: List[str]
            instance name of the things to be served as a list.
        port: int, default 8080
            the port at which the server should be run
        address: str, default 0.0.0.0
            IP address
        logger: logging.Logger, optional
            logging.Logger instance
        log_level: int
            alternative to logger, this creates an internal logger with the specified log level along with a IO stream handler. 
        serializer: JSONSerializer, optional
            json serializer used by the server
        ssl_context: ssl.SSLContext
            SSL context to provide encrypted communication
        certfile: str
            alternative to SSL context, provide certificate file & key file to allow the server to create a SSL context 
        keyfile: str
            alternative to SSL context, provide certificate file & key file to allow the server to create a SSL context 
        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:
            rpc_handler: RPCHandler | BaseHandler, optional
                custom web request handler of your choice for property read-write & action execution
            event_handler: EventHandler | BaseHandler, optional
                custom event handler of your choice for handling events
        """
        super().__init__(
            things=things,
            port=port, 
            address=address, 
            host=host,
            logger=logger, 
            log_level=log_level,
            serializer=serializer or JSONSerializer(), 
            # protocol_version=1, 
            schema_validator=schema_validator,
            certfile=certfile, 
            keyfile=keyfile,
            ssl_context=ssl_context,
            # network_interface='Ethernet',# network_interface,
            property_handler=kwargs.get('property_handler', PropertyHandler),
            action_handler=kwargs.get('action_handler', ActionHandler),
            event_handler=kwargs.get('event_handler', EventHandler),
            allowed_clients=allowed_clients if allowed_clients is not None else []
        )

        self._IP = f"{self.address}:{self.port}"
        if self.logger is None:
            self.logger = get_default_logger('{}|{}'.format(self.__class__.__name__, 
                                            f"{self.address}:{self.port}"), 
                                            self.log_level)

        self.app = Application(handlers=[
            (r'/things', ThingsHandler, dict(owner=self)),
            (r'/stop', StopHandler, dict(owner=self))
        ])

        self.zmq_client_pool = MessageMappedZMQClientPool(
                                                    id=self._IP,
                                                    server_ids=[],
                                                    client_ids=[],
                                                    handshake=False,
                                                    poll_timeout=100,                                                   
                                                    logger=self.logger
                                                )

        self._type = HTTPServerTypes.THING_SERVER
        self._disconnected_things = dict() # see update_router_with_thing

        self._zmq_protocol = ZMQ_TRANSPORTS.IPC
        self._zmq_inproc_socket_context = None 
        self._zmq_inproc_event_context = None

        self._checked = False


    @property
    def all_ok(self) -> bool:
        """
        check if all the requirements are met before starting the server, auto invoked by listen().
        """
        if self._checked:
            return True
        # print("client pool context", self.zmq_client_pool.context)
        event_loop = get_current_async_loop() # sets async loop for a non-possessing thread as well
        event_loop.call_soon(lambda : asyncio.create_task(self.update_router_with_things()))
        event_loop.call_soon(lambda : asyncio.create_task(self.subscribe_to_host()))
        event_loop.call_soon(lambda : asyncio.create_task(self.zmq_client_pool.poll_responses()) )
        for client in self.zmq_client_pool: 
            event_loop.call_soon(lambda : asyncio.create_task(client._handshake(timeout=60000)))

        self.tornado_event_loop = None 
        # set value based on what event loop we use, there is some difference 
        # between the asyncio event loop and the tornado event loop

        # if self.protocol_version == 2:
        #     raise NotImplementedError("Current HTTP2 is not implemented.")
        #     self.tornado_instance = TornadoHTTP2Server(self.app, ssl_options=self.ssl_context)
        # else:
        self.tornado_instance = TornadoHTTP1Server(self.app, ssl_options=self.ssl_context)
        self._checked = True
        return True


    def listen(self) -> None:
        """
        Start the HTTP server. This method is blocking. Async event loops intending to schedule the HTTP server should instead use
        the inner tornado instance's (``HTTPServer.tornado_instance``) listen() method. 
        """
        if not self._checked:
            assert self.all_ok, 'HTTPServer all is not ok before starting' # Will always be True or cause some other exception   
        self.tornado_event_loop = ioloop.IOLoop.current()
        self.tornado_instance.listen(port=self.port, address=self.address)    
        self.logger.info(f'started webserver at {self._IP}, ready to receive requests.')
        self.tornado_event_loop.start()


    def 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. 
        """
        self.tornado_instance.stop()
        self.zmq_client_pool.stop_polling()
        run_callable_somehow(self.tornado_instance.close_all_connections())
        if self.tornado_event_loop is not None:
            self.tornado_event_loop.stop()


    async def _stop_async(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. 
        """
        self.tornado_instance.stop()
        self.zmq_client_pool.stop_polling()
        await self.tornado_instance.close_all_connections()
        if self.tornado_event_loop is not None:
            self.tornado_event_loop.stop()


    def add_things(self, *things: Thing | ThingMeta | dict | str) -> None:
        """
        Add things to be served by the HTTP server

        Parameters
        ----------
        *things: Thing | ThingMeta | dict | str
            the thing instance(s) or thing classe(s) to be served, or a map of address/ZMQ protocol to thing id, 
            for example - {'tcp://my-pc:5555': 'my-thing-id', 'IPC' : 'my-thing-id-2'}
        """
        for thing in things:
            if isinstance(thing, Thing): 
                add_thing_instance(self, thing)
            elif isinstance(thing, dict):
                add_zmq_served_thing(self, thing)
            elif issubklass(thing, ThingMeta):
                raise TypeError(f"thing should be of type Thing or ThingMeta, given type {type(thing)}")


    def add_thing(self, thing: Thing | ThingMeta | dict | str) -> None:
        """
        Add thing to be served by the HTTP server

        Parameters
        ----------
        thing: str | Thing | ThingMeta
            id of the thing or the thing instance or thing class to be served
        """
        self.add_things(thing)


    def add_property(self, 
                    URL_path: str, 
                    property: Property | PropertyAffordance, 
                    http_methods: typing.Tuple[str, typing.Optional[str], typing.Optional[str]] | None = ('GET', 'PUT', None), 
                    handler: BaseHandler | PropertyHandler = 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__
        """
        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)}")
        http_methods = _comply_http_method(http_methods)
        read_http_method = write_http_method = delete_http_method = None
        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")
        if isinstance(property, Property):
            property = property.to_affordance()
            property._build_forms()
        kwargs['resource'] = property
        kwargs['owner'] = self
        for rule in self.app.wildcard_router.rules:
            if rule.matcher == URL_path:
                warnings.warn(f"property {property.name} already exists in the router - replacing it.",
                        category=UserWarning)
                # raise ValueError(f"URL path {URL_path} already exists in the router")
        self.app.wildcard_router.add_rules([(URL_path, handler, kwargs)])

        """
        for handler based tornado rule matcher, the Rule object has following
        signature

        def __init__(
            self,
            matcher: "Matcher",
            target: Any,
            target_kwargs: Optional[Dict[str, Any]] = None,
            name: Optional[str] = None,
        ) -> None:

        matcher - based on route
        target - handler
        target_kwargs - given to handler's initialize
        name - ...

        len == 2 tuple is route + handler
        len == 3 tuple is route + handler + target kwargs

        so we give (path, BaseHandler, {'resource' : PropertyAffordance, 'owner' : self})

        path is extracted from interaction affordance name or given by the user
        BaseHandler is the base handler of this package for interaction affordances
        resource goes into target kwargs which is needed for the handler to work correctly
        """


    def add_action(self, 
                URL_path: str, 
                action: Action | ActionAffordance, 
                http_method: str | None = 'POST',
                handler: BaseHandler | ActionHandler = 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__
        """
        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 = _comply_http_method(http_method)
        if len(http_methods) != 1:
            raise ValueError("http_method should be a single HTTP method")
        if isinstance(action, Action):
            action = action.to_affordance() # type: ActionAffordance
            action._build_forms()
        kwargs['resource'] = action
        kwargs['owner'] = self
        for rule in self.app.wildcard_router.rules:
            if rule.matcher == URL_path:
                warnings.warn(f"URL path {URL_path} already exists in the router -" +
                        " replacing it for action {action.name}", category=UserWarning)
        self.app.wildcard_router.add_rules([(URL_path, handler, kwargs)])


    def add_event(self, 
                URL_path: str, 
                event: Event | EventAffordance, 
                handler: BaseHandler | EventHandler = 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__
        """
        if not isinstance(event, (Event, EventAffordance)):
            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):
            event = event.to_affordance()
            event._build_forms()
        kwargs['resource'] = event
        kwargs['owner'] = self
        for rule in self.app.wildcard_router.rules:
            if rule.matcher == URL_path:
                warnings.warn(f"URL path {URL_path} already exists in the router -" + 
                        " replacing it for event {event.friendly_name}", category=UserWarning)
        self.app.wildcard_router.add_rules([(URL_path, handler, kwargs)])


    async def subscribe_to_host(self):
        if self.host is None:
            return
        client = AsyncHTTPClient()
        for i in range(300): # try for five minutes
            try:
                res = await client.fetch(HTTPRequest(
                        url=f"{self.host}/subscribers",
                        method='POST',
                        body=JSONSerializer.dumps(dict(
                                hostname=socket.gethostname(),
                                IPAddress=get_IP_from_interface(self.network_interface), 
                                port=self.port, 
                                type=self._type,
                                https=self.ssl_context is not None 
                            )),
                        validate_cert=False,
                        headers={"content-type" : "application/json"}
                    ))
            except Exception as ex:
                self.logger.error(f"Could not subscribe to host {self.host}. error : {str(ex)}, error type : {type(ex)}.")
                if i >= 299:
                    raise ex from None
            else: 
                if res.code in [200, 201]:
                    self.logger.info(f"subsribed successfully to host {self.host}")
                    break
                elif i >= 299:
                    raise RuntimeError(f"could not subsribe to host {self.host}. response {JSONSerializer.loads(res.body)}")
            await asyncio.sleep(1)
        # we lose the client anyway so we close it. if we decide to reuse the client, changes needed
        client.close() 


    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})"

    def __del__(self):
        self.stop()

Attributes

all_ok property

all_ok: bool

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

Functions

__init__

__init__(things: typing.List[str] | typing.List[Thing] | typing.List[ThingMeta] | None = None, *, port: int = 8080, address: str = '0.0.0.0', host: typing.Optional[str] = None, logger: typing.Optional[logging.Logger] = None, log_level: int = logging.INFO, serializer: typing.Optional[JSONSerializer] = None, ssl_context: typing.Optional[ssl.SSLContext] = None, schema_validator: typing.Optional[BaseSchemaValidator] = JSONSchemaValidator, certfile: str = None, keyfile: str = None, allowed_clients: typing.Optional[typing.Union[str, typing.Iterable[str]]] = None, **kwargs) -> None

Parameters:

Name Type Description Default

things

List[str] | List[Thing] | List[ThingMeta] | None

instance name of the things to be served as a list.

None

port

int

the port at which the server should be run

8080

address

str

IP address

'0.0.0.0'

logger

Optional[Logger]

logging.Logger instance

None

log_level

int

alternative to logger, this creates an internal logger with the specified log level along with a IO stream handler.

INFO

serializer

Optional[JSONSerializer]

json serializer used by the server

None

ssl_context

Optional[SSLContext]

SSL context to provide encrypted communication

None

certfile

str

alternative to SSL context, provide certificate file & key file to allow the server to create a SSL context

None

keyfile

str

alternative to SSL context, provide certificate file & key file to allow the server to create a SSL context

None

allowed_clients

Optional[Union[str, Iterable[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.

None

**kwargs

rpc_handler: RPCHandler | BaseHandler, optional custom web request handler of your choice for property read-write & action execution event_handler: EventHandler | BaseHandler, optional custom event handler of your choice for handling events

{}
Source code in hololinked\protocols\http\server\__init__.py
def __init__(self, 
            things : typing.List[str] | typing.List[Thing] | typing.List[ThingMeta] | None = None, *, 
            port : int = 8080, address : str = '0.0.0.0', host : typing.Optional[str] = None, 
            logger : typing.Optional[logging.Logger] = None, log_level : int = logging.INFO, 
            serializer : typing.Optional[JSONSerializer] = None, ssl_context : typing.Optional[ssl.SSLContext] = None, 
            schema_validator : typing.Optional[BaseSchemaValidator] = JSONSchemaValidator,
            certfile : str = None, keyfile : str = None, 
            # protocol_version : int = 1, network_interface : str = 'Ethernet', 
            allowed_clients : typing.Optional[typing.Union[str, typing.Iterable[str]]] = None,   
            **kwargs
        ) -> None:
    """
    Parameters
    ----------
    things: List[str]
        instance name of the things to be served as a list.
    port: int, default 8080
        the port at which the server should be run
    address: str, default 0.0.0.0
        IP address
    logger: logging.Logger, optional
        logging.Logger instance
    log_level: int
        alternative to logger, this creates an internal logger with the specified log level along with a IO stream handler. 
    serializer: JSONSerializer, optional
        json serializer used by the server
    ssl_context: ssl.SSLContext
        SSL context to provide encrypted communication
    certfile: str
        alternative to SSL context, provide certificate file & key file to allow the server to create a SSL context 
    keyfile: str
        alternative to SSL context, provide certificate file & key file to allow the server to create a SSL context 
    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:
        rpc_handler: RPCHandler | BaseHandler, optional
            custom web request handler of your choice for property read-write & action execution
        event_handler: EventHandler | BaseHandler, optional
            custom event handler of your choice for handling events
    """
    super().__init__(
        things=things,
        port=port, 
        address=address, 
        host=host,
        logger=logger, 
        log_level=log_level,
        serializer=serializer or JSONSerializer(), 
        # protocol_version=1, 
        schema_validator=schema_validator,
        certfile=certfile, 
        keyfile=keyfile,
        ssl_context=ssl_context,
        # network_interface='Ethernet',# network_interface,
        property_handler=kwargs.get('property_handler', PropertyHandler),
        action_handler=kwargs.get('action_handler', ActionHandler),
        event_handler=kwargs.get('event_handler', EventHandler),
        allowed_clients=allowed_clients if allowed_clients is not None else []
    )

    self._IP = f"{self.address}:{self.port}"
    if self.logger is None:
        self.logger = get_default_logger('{}|{}'.format(self.__class__.__name__, 
                                        f"{self.address}:{self.port}"), 
                                        self.log_level)

    self.app = Application(handlers=[
        (r'/things', ThingsHandler, dict(owner=self)),
        (r'/stop', StopHandler, dict(owner=self))
    ])

    self.zmq_client_pool = MessageMappedZMQClientPool(
                                                id=self._IP,
                                                server_ids=[],
                                                client_ids=[],
                                                handshake=False,
                                                poll_timeout=100,                                                   
                                                logger=self.logger
                                            )

    self._type = HTTPServerTypes.THING_SERVER
    self._disconnected_things = dict() # see update_router_with_thing

    self._zmq_protocol = ZMQ_TRANSPORTS.IPC
    self._zmq_inproc_socket_context = None 
    self._zmq_inproc_event_context = None

    self._checked = False

add_things

add_things(*things: Thing | ThingMeta | dict | str) -> None

Add things to be served by the HTTP server

Parameters:

Name Type Description Default

*things

Thing | ThingMeta | dict | str

the thing instance(s) or thing classe(s) to be served, or a map of address/ZMQ protocol to thing id, for example - {'tcp://my-pc:5555': 'my-thing-id', 'IPC' : 'my-thing-id-2'}

()
Source code in hololinked\protocols\http\server\__init__.py
def add_things(self, *things: Thing | ThingMeta | dict | str) -> None:
    """
    Add things to be served by the HTTP server

    Parameters
    ----------
    *things: Thing | ThingMeta | dict | str
        the thing instance(s) or thing classe(s) to be served, or a map of address/ZMQ protocol to thing id, 
        for example - {'tcp://my-pc:5555': 'my-thing-id', 'IPC' : 'my-thing-id-2'}
    """
    for thing in things:
        if isinstance(thing, Thing): 
            add_thing_instance(self, thing)
        elif isinstance(thing, dict):
            add_zmq_served_thing(self, thing)
        elif issubklass(thing, ThingMeta):
            raise TypeError(f"thing should be of type Thing or ThingMeta, given type {type(thing)}")

add_thing

add_thing(thing: Thing | ThingMeta | dict | str) -> None

Add thing to be served by the HTTP server

Parameters:

Name Type Description Default

thing

Thing | ThingMeta | dict | str

id of the thing or the thing instance or thing class to be served

required
Source code in hololinked\protocols\http\server\__init__.py
def add_thing(self, thing: Thing | ThingMeta | dict | str) -> None:
    """
    Add thing to be served by the HTTP server

    Parameters
    ----------
    thing: str | Thing | ThingMeta
        id of the thing or the thing instance or thing class to be served
    """
    self.add_things(thing)

add_property

add_property(URL_path: str, property: Property | PropertyAffordance, http_methods: typing.Tuple[str, typing.Optional[str], typing.Optional[str]] | None = ('GET', 'PUT', None), handler: BaseHandler | PropertyHandler = 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

Tuple[str, Optional[str], Optional[str]] | None

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', None)

handler

BaseHandler | PropertyHandler

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

{}
Source code in hololinked\protocols\http\server\__init__.py
def add_property(self, 
                URL_path: str, 
                property: Property | PropertyAffordance, 
                http_methods: typing.Tuple[str, typing.Optional[str], typing.Optional[str]] | None = ('GET', 'PUT', None), 
                handler: BaseHandler | PropertyHandler = 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__
    """
    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)}")
    http_methods = _comply_http_method(http_methods)
    read_http_method = write_http_method = delete_http_method = None
    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")
    if isinstance(property, Property):
        property = property.to_affordance()
        property._build_forms()
    kwargs['resource'] = property
    kwargs['owner'] = self
    for rule in self.app.wildcard_router.rules:
        if rule.matcher == URL_path:
            warnings.warn(f"property {property.name} already exists in the router - replacing it.",
                    category=UserWarning)
            # raise ValueError(f"URL path {URL_path} already exists in the router")
    self.app.wildcard_router.add_rules([(URL_path, handler, kwargs)])

    """
    for handler based tornado rule matcher, the Rule object has following
    signature

    def __init__(
        self,
        matcher: "Matcher",
        target: Any,
        target_kwargs: Optional[Dict[str, Any]] = None,
        name: Optional[str] = None,
    ) -> None:

    matcher - based on route
    target - handler
    target_kwargs - given to handler's initialize
    name - ...

    len == 2 tuple is route + handler
    len == 3 tuple is route + handler + target kwargs

    so we give (path, BaseHandler, {'resource' : PropertyAffordance, 'owner' : self})

    path is extracted from interaction affordance name or given by the user
    BaseHandler is the base handler of this package for interaction affordances
    resource goes into target kwargs which is needed for the handler to work correctly
    """

add_action

add_action(URL_path: str, action: Action | ActionAffordance, http_method: str | None = 'POST', handler: BaseHandler | ActionHandler = 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 | None

http method to be used for the action

'POST'

handler

BaseHandler | ActionHandler

custom handler for the action

ActionHandler

kwargs

dict

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

{}
Source code in hololinked\protocols\http\server\__init__.py
def add_action(self, 
            URL_path: str, 
            action: Action | ActionAffordance, 
            http_method: str | None = 'POST',
            handler: BaseHandler | ActionHandler = 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__
    """
    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 = _comply_http_method(http_method)
    if len(http_methods) != 1:
        raise ValueError("http_method should be a single HTTP method")
    if isinstance(action, Action):
        action = action.to_affordance() # type: ActionAffordance
        action._build_forms()
    kwargs['resource'] = action
    kwargs['owner'] = self
    for rule in self.app.wildcard_router.rules:
        if rule.matcher == URL_path:
            warnings.warn(f"URL path {URL_path} already exists in the router -" +
                    " replacing it for action {action.name}", category=UserWarning)
    self.app.wildcard_router.add_rules([(URL_path, handler, kwargs)])

add_event

add_event(URL_path: str, event: Event | EventAffordance, handler: BaseHandler | EventHandler = 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

Event (object) to be served or its JSON representation

required

handler

BaseHandler | EventHandler

custom handler for the event

EventHandler

kwargs

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

{}
Source code in hololinked\protocols\http\server\__init__.py
def add_event(self, 
            URL_path: str, 
            event: Event | EventAffordance, 
            handler: BaseHandler | EventHandler = 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__
    """
    if not isinstance(event, (Event, EventAffordance)):
        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):
        event = event.to_affordance()
        event._build_forms()
    kwargs['resource'] = event
    kwargs['owner'] = self
    for rule in self.app.wildcard_router.rules:
        if rule.matcher == URL_path:
            warnings.warn(f"URL path {URL_path} already exists in the router -" + 
                    " replacing it for event {event.friendly_name}", category=UserWarning)
    self.app.wildcard_router.add_rules([(URL_path, handler, kwargs)])

listen

listen() -> None

Start the HTTP server. This method is blocking. Async event loops intending to schedule the HTTP server should instead use the inner tornado instance's (HTTPServer.tornado_instance) listen() method.

Source code in hololinked\protocols\http\server\__init__.py
def listen(self) -> None:
    """
    Start the HTTP server. This method is blocking. Async event loops intending to schedule the HTTP server should instead use
    the inner tornado instance's (``HTTPServer.tornado_instance``) listen() method. 
    """
    if not self._checked:
        assert self.all_ok, 'HTTPServer all is not ok before starting' # Will always be True or cause some other exception   
    self.tornado_event_loop = ioloop.IOLoop.current()
    self.tornado_instance.listen(port=self.port, address=self.address)    
    self.logger.info(f'started webserver at {self._IP}, ready to receive requests.')
    self.tornado_event_loop.start()

stop

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 hololinked\protocols\http\server\__init__.py
def 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. 
    """
    self.tornado_instance.stop()
    self.zmq_client_pool.stop_polling()
    run_callable_somehow(self.tornado_instance.close_all_connections())
    if self.tornado_event_loop is not None:
        self.tornado_event_loop.stop()