The uREST Server Implementation

Overview

The internal network interface and helper classes for abstracting the underlying HTTP stream live in the http package. When creating an API using the urest library, the http package provides the abstraction of the underlying socket handling code. Specifically all marshaling requests from the network clients, calling of the library users API classes, and responses to the network clients passes through the http package. All low-level code for the socket handling in http is built around the Python 3 asyncio library, and the library user API is similarly expected to be based around the use of co- routines to simplify request handling.

In most cases, library users only need to provide an asynio event loop, and the bind the RESTServer from the http module to that loop. The internal details of the http module are not needed in most use-cases: see the Creating a Network Server How- To_for more details.

By default the module will also bind to the network address of the host on the standard port 80 used by HTTP requests. This can be changed in the instantiation of the RESTServer class, and before binding to the asyncio loop.

Package and Class Layout

The core classes of the HTTP module are organised as follows. In most cases only the RESTServer class will be directly used by library users.

Package Layout for urest.http

Example Implementation

Getting the Noun State

This example is based on the SimpleLED class as a minimal implementation of a noun controlling a GPIO pin, using the MicroPython Pin library. The full documentation for an example based on the SimpleLED class is available through the urest.examples package. Details of the calls needed to set-up the RESTServer are also provided in the Creating a Network Server How-To.

The following sequence diagrams assume that a RESTServer has been created as

app = RESTServer()

and the SimpleLED class subsequently registred as the ‘noun’ led via the RESTServer.register_noun() method as

app.register_noun('led', SimpleLED(28))

The calling application then hands over to the app instance (of the RESTServer) class via the start() method of the app instance

  if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.create_task(app.start())
    loop.run_forever()

Subsequently a client on the network wants to query the state of the led via the HTTP request to the server

GET /led HTTP 1.1

On receiving the above request from the client, the RESTServer instance will call the relevant methods of the SimpleLED (as a sub-class of APIBase) as shown in Figure 1. Note that the ‘<< resource state>>’ returned by SimpleLED.get_state() is as a Python dict[str, Union[str, int]]. The << headers >> are also assumed to follow the HTTP/1.1 specification; whether read from the HTTP response itself, or stored internally by HTTPResponse.

Figure 1: A Sequence Diagram for the Noun Get Request

Figure 1: A Sequence Diagram for the Noun Get Request of the Example

The initial network request from the client is handled by the event loop of the asyncio library, which will create instances of asyncio.StreamReader and asyncio.StreamWriter to represent data being read from, or written to, the network respectively. Once this is done, the asyncio library will call RESTServer.dispatch_noun() to handle the request.

The RESTServer.dispatch_noun(), in turn, creates an instances of the HTTPResponse class. This instance of HTTPResponse is then set-up from the data read from the HTTP headers via the asyncio.StreamReader.

With the inital set-up complete, RESTServer.dispatch_noun() calls the relevant handler for the request. In this example SimpleLED.get_state() from an instance of SimpleLED set-up earlier.

Once the SimpleLED.get_state() method completes, the returned data is parsed by the RESTServer.dispatch_noun(). Valid responses from the SimpleLED.get_state() method are then used to complete the set-up of the HTTPResponse instance. Finally HTTPResponse.send() is called to return the data to the client via the asyncio.StreamWriter.

Setting the Noun State

Setting the state of the noun is a little more involved on the client side, as this will require the desired state of the noun to be sent to the server. A useful tool for testing purposes is the curl utility; available on most platforms.

Continuing the minimal example above, the command line

curl -X PUT -d '{"led":"0"}' -H "Content-Type: application/json" http://10.0.30.225/LED

will transmit something like the following HTTP request to RESTServer

PUT /LED HTTP/1.1

Host: 10.0.30.225
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 11

{"led":"0"}

JSON is Required

The only format accepted by the RESTServer for the state of the nouns is JSON. The RESTServer will also only accept a sub-set of the JSON standard: notably assuming a single object, and a collection of key/value pairs.

Figure 2: A Sequence Diagram for the Noun Set Request

Figure 2: A Sequence Diagram for the Noun Set Request of the Example

As before, the initial network request from the client is handled by the event loop of the asyncio library, which will create instances of asyncio.StreamReader and asyncio.StreamWriter to represent data being read from, or written to, the network respectively. Once this is done, the asyncio library will call RESTServer.dispatch_noun() to handle the request.

The RESTServer.dispatch_noun(), in turn, creates an instances of the HTTPResponse class. This instance of HTTPResponse is then set-up from the data read from the HTTP headers via the asyncio.StreamReader. Since this is a PUT request, the RESTServer.dispatch_noun() will also read the HTTP body from the client, parse into a JSON structure, and then use this to set the desired << resource_state >> to pass onto the noun handling the request.

With the inital set-up complete, RESTServer.dispatch_noun() calls the relevant handler for the request. In this example SimpleLED.set_state() from an instance of SimpleLED set-up earlier.

The exact interpretation of the JSON << resource_state >> from the Client in Figure 2 is left to the implementation of the noun. The only internal guarantee provided by the library is the JSON will be parsed as far as possible and sent to SimpleLED.set_state(), in this case, as a Python dict[str, Union[str, int]]. For instance in the above example the key led is interpreted as referring to the noun, and the value 0 as the state value. In this case setting the GPIO Pin 28 to the value 0 (or off). A close correspondence between the name of the noun as referred to the API, and the name of the noun in the state list is strongly advised: but is not strictly required.

Once the SimpleLED.set_state() method completes, RESTServer.dispatch_noun() passes contol onto HTTPResponse via the HTTPResponse.send(). The HTTPResponse.send() passes any additional headers to the client via the asyncio.StreamWriter: but otherwise completes with the HTTP response 200 OK.

Tested Implementations

This version is written for MicroPython 3.4, and has been tested on:

  • Raspberry Pi Pico W

Enumerations

urest.http.response.HTTPStatus

Bases: IntEnum

Enumeration defining the valid HTTP responses, and the associated response codes to use.

Principally used internally by the RESTServer class when building an instance of the HTTPResponse class when returning data to the client. The enumerated values are used as the HTTP response status code for that client response and should follow the relevant standards. See the Mozilla HTTP response status codes for more details.

Source code in urest/http/response.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class HTTPStatus(IntEnum):
    """Enumeration defining the valid HTTP responses, and the associated
    response codes to use.

    Principally used internally by the [`RESTServer`][urest.http.RESTServer]
    class when building an instance of the [`HTTPResponse`]
    [urest.http.HTTPResponse] class when returning data to the client. The
    enumerated values are used as the HTTP response status code for that
    client response and should follow the relevant standards. See the Mozilla [HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) for more details.
    """

    OK = 200
    NOT_OK = 400
    NOT_FOUND = 404

Classes

urest.http.RESTServer

Initialise the server with reasonable defaults. These should work for most cases, and should be set so that most clients won’t have to touch them.

The RESTServer class acts as the primary interface to the library, handling all the network communication with the client, formatting the response and marshalling the API calls required to generate that response.

In most cases consumers of this module will create a single instance of the RESTServer class, and then pass the reference to the RESTServer.start() method to an event loop of the asyncio library.

For example the following code creates a variable app for the instance of the RESTServer class, and passes this to the ‘main’ event loop of the asyncio library

app = RESTServer()


if __name__ == "__main__":
  loop = asyncio.get_event_loop()
  loop.create_task(app.start())
  loop.run_forever()

The RESTServer.start() method is expected to be used in a asyncio event loop, as above; with the tasks being handled by the RESTServer.dispatch_noun() method. If the event loop is required to be closed, or destroyed, the tasks can be removed using the RESTServer.stop() method.

Note

The code in this class assumes the asyncio library with an interface roughly equivalent to Python 3.4: although the MicroPython module supports some later extensions. Given the code churn in the asyncio module between Python 3.4 and Python 3.10, careful testing is required to ensure implementation compatibility.

Attributes:
  • host (string) –

    A resolvable DNS host name or IP address. Note that the exact requirements are determined by the asyncio.BaseEventLoop.create_server method, which should be checked carefully for implementation defined limitations.

    Default: An IPv4 sock on the local host.

  • port (integer) –

    The local (server) port to bind the socket to. Note that the exact requirements are determined by the asyncio.BaseEventLoop.create_server method, which should be checked carefully for implementation defined limitations (e.g. extra privileges required for system ports).

    Default: The IANA Assigned port 80 for an HTTP Server.

  • backlog (integer) –

    Roughly the size of the pool of connections for the underlying socket. Once this value has been exceeded, the tasks will be suspended by the co-routine handler until the underlying socket can clear them. Note that the size (and interpretation) of this value is system dependent: see the socket API for more details.

    Default: 5 (typically the maximum pool size allowed).

  • read_timeout (integer) –

    Length of time in seconds to wait for a response from the client before declaring failure.

    Default: 30 seconds.

  • write_timeout (integer) –

    Length of time in seconds to wait for the network socket to accept a write to the client, before declaring failure.

Methods:

  • * `dispatch_noun

    Handles the network request from the client, and marshals that request to the appropriate handler APIBase handler further processing.

  • * `register_noun

    Set-up a handler for a instance of APIBase to respond to the specified noun from the network client.

  • * `start

    Begin the event processing loop, responding to requests from network clients.

  • * `stop:`

    Stop processing events and responding to requests from the network clients.

Source code in urest/http/server.py
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
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
class RESTServer:
    """Initialise the server with reasonable defaults. These should work for
    most cases, and should be set so that most clients won't have to touch
    them.

    The [`RESTServer`][urest.http.server.RESTServer] class acts as the primary interface to
    the library, handling all the network communication with the client,
    formatting the response and marshalling the API calls required to generate
    that response.

    In most cases consumers of this module will create a single instance of
    the [`RESTServer`][urest.http.server.RESTServer] class, and then pass the reference to
    the [`RESTServer.start()`][urest.http.server.RESTServer.start] method to an event loop of the
    `asyncio` library.

    For example the following code creates a variable `app` for the instance
    of the [`RESTServer`][urest.http.server.RESTServer] class, and passes this to the 'main'
    event loop of the `asyncio` library

    ```python
    app = RESTServer()


    if __name__ == "__main__":
      loop = asyncio.get_event_loop()
      loop.create_task(app.start())
      loop.run_forever()
    ```

    The [`RESTServer.start()`][urest.http.server.RESTServer.start] method is expected to be used in
    a [`asyncio` event loop](https://docs.python.org/3.4/library/asyncio-
    eventloop.html), as above; with the tasks being handled by the
    [`RESTServer.dispatch_noun()`][urest.http.server.RESTServer.dispatch_noun] method. If the event loop is
    required to be closed, or destroyed, the tasks can be removed using the
    [`RESTServer.stop()`][urest.http.server.RESTServer.stop] method.

    !!! Note
        The code in this class assumes the `asyncio` library with an interface
        roughly equivalent to Python 3.4: although the MicroPython module
        supports _some_ later extensions. Given the code churn in the
        `asyncio` module between Python 3.4 and Python 3.10, careful testing
        is required to ensure implementation compatibility.

    Attributes
    ----------
        host: string
        A resolvable DNS host name or IP address. Note that the exact
        requirements are determined by the
        [`asyncio.BaseEventLoop.create_server`](https://docs.python.org/3.4/
        library/asyncio-eventloop.html#asyncio.BaseEventLoop.create_server)
        method, which should be checked carefully for implementation defined
        limitations.

        **Default:** An IPv4 sock on the local host.
    port: integer
        The local (server) port to bind the socket to. Note that the exact
        requirements are determined by the [`asyncio.BaseEventLoop.create_server`](https://docs.python.org/3.4/library/asyncio-eventloop.html#asyncio.BaseEventLoop.create_server)
        method, which should be checked carefully for implementation defined
        limitations (e.g. extra privileges required for system ports).

        **Default:** The IANA Assigned port 80 for an HTTP Server.
    backlog: integer
        Roughly the size of the pool of connections for the underlying
        `socket`. Once this value has been exceeded, the tasks will be
        suspended by the co-routine handler until the underlying `socket` can
        clear them. Note that the size (and interpretation) of this value is
        system dependent: see the [`socket` API](https://docs.python.org/3.4/library/socket.html#module-socket)
        for more details.

        **Default:** 5 (typically the maximum pool size allowed).
    read_timeout: integer
        Length of time in seconds to wait for a response from the client before declaring
        failure.

        **Default:** 30 seconds.
    write_timeout: integer
        Length of time in seconds to wait for the network socket to accept a write to the
        client, before declaring failure.

    Methods
    -------

    * `dispatch_noun()`:
        Handles the network request from the client, and marshals that request
        to the appropriate handler [`APIBase`][urest.api.base.APIBase] handler further processing.
    * `register_noun():`
        Set-up a handler for a instance of [`APIBase`][urest.api.base.APIBase]
        to respond to the specified noun from the network client.
    * `start():`
        Begin the event processing loop, responding to requests from network
        clients.
    * `stop:`
        Stop processing events and responding to requests from the network
        clients.

    """

    ##
    ## Attributes
    ##

    _nouns: dict[str, APIBase]
    """The list of registered objects which should be called when the given
    name is passed in the URI."""

    ##
    ## Constructor
    ##

    def __init__(
        self,
        host: str = "0.0.0.0",
        port: int = 80,
        backlog: int = 5,
        read_timeout: int = 30,
        write_timeout: int = 5,
    ) -> None:
        """Create an instance of the `RESTServer` class to handle client
        requests. In most cases there should only be once instance of
        `RESTServer` in each application (i.e. `RESTServer` should be treated
        as a singleton).

        Parameters
        ----------

        host: string
            A resolvable DNS host name or IP address. Note that the exact
            requirements are determined by the
            [`asyncio.BaseEventLoop.create_server`](https://docs.python.org/3.4/
            library/asyncio-eventloop.html#asyncio.BaseEventLoop.create_server)
            method, which should be checked carefully for implementation defined
            limitations.

            **Default:** An IPv4 sock on the local host.
        port: integer
            The local (server) port to bind the socket to. Note that the exact
            requirements are determined by the [`asyncio.BaseEventLoop.create_server`](https://docs.python.org/3.4/library/asyncio-eventloop.html#asyncio.BaseEventLoop.create_server)
            method, which should be checked carefully for implementation defined
            limitations (e.g. extra privileges required for system ports).

            **Default:** The IANA Assigned port 80 for an HTTP Server.
        backlog: integer
            Roughly the size of the pool of connections for the underlying
            `socket`. Once this value has been exceeded, the tasks will be
            suspended by the co-routine handler until the underlying `socket` can
            clear them. Note that the size (and interpretation) of this value is
            system dependent: see the [`socket` API](https://docs.python.org/3.4/library/socket.html#module-socket)
            for more details.

            **Default:** 5 (typically the maximum pool size allowed).
        read_timeout: integer
            Length of time in seconds to wait for a response from the client before declaring
            failure.

            **Default:** 30 seconds.
        write_timeout: integer
            Length of time in seconds to wait for the network socket to accept a write to the
            client, before declaring failure.

            **Default:** 5 seconds.

        """
        self.host = host
        self.port = port
        self.backlog = backlog
        self.read_timeout = read_timeout
        self.write_timeout = write_timeout
        self._server = None
        self._nouns = {"": APIBase()}

    def _parse_data(self, data_str: str) -> dict[str, Union[str, int]]:
        """Attempt to parse a string containing JSON-like formatting into a
        single dictionary.

        This function is **very** far from a full JSON parser: quite deliberately
        it will only accept a single object of name/value pairs. Any arrays are
        **not** accepted. In addition, this parser will coerce the 'name' side of
        the dictionary into a string; or if this cannot be done will raise a
        `RESTParseError`. The 'value' of a name/value pair will like-wise be
        coerced into its JSON type; or a `RESTParseError` raised if this cannot be
        done.

        The parsing is done via a very simple stack-based parser, assuming no
        backtracking. This will cope with valid JSON: but will quickly abort if
        the JSON is malformed, raising a `RESTParseError`. This is quite
        deliberate as we will only accept valid JSON from the client: if we can't
        parse the result that is the clients problem...

        The parser will also finish after the first found JSON object. We are
        expecting only a single dictionary from the client, and so attempts to add
        something more exotic will be ignored.

        Parameters
        ----------

        data_str: str
            A JSON object string, representing a single dictionary

        Raises
        ------

        RESTParseError

        Returns
        -------

        dict[str, Union[str, int]]
            A mapping of (key, value) pairs which defines the dictionary of the
            `data_str` object. All `key` values will be in Python string format:
            values will be as defined in the `data_str` object.

        """

        return_dictionary: dict[str, Union[str, int]] = {}
        parse_stack = []
        object_start = False

        token_start = False
        token_sep = False

        token_type = JSON_TYPE_INT
        token_str = ""

        for char in data_str:
            # Look for the first object
            if char in ["{"]:
                object_start = True

            # If we are inside an object, attempt to assemble the
            # tokens
            if object_start:
                # If a '"' is found...
                if char in ['"']:
                    # ... and if we are building a token, this should be the
                    # end of a string, so push it to the stack ...
                    if token_start:
                        if token_type == JSON_TYPE_STR:
                            parse_stack.append(token_str)

                            token_start = False
                        else:
                            msg = "Invalid string termination"
                            raise RESTParseError(msg)

                        token_type = JSON_TYPE_INT
                        token_sep = False
                        token_start = False
                        token_str = ""

                    # ... Otherwise if we are not building a token, set the
                    # type marker, and start building a new string
                    else:
                        token_type = JSON_TYPE_STR
                        token_start = True
                        token_str = ""

                # Look for the separator between a 'key' and a 'value'
                if char in [":"]:
                    token_sep = True

                # Look for the end of a token
                if char in [",", "}"]:
                    # Add the key/value to the return dictionary if
                    # it appears to be valid
                    if token_sep:
                        if token_type == JSON_TYPE_INT:
                            # The token won't have been terminated
                            # so still should be in `token_str`
                            #
                            # NOTE: Technically an un-terminated String
                            #       is an error, so will parsing will
                            #       break here (and we won't care)
                            parse_stack.append(token_str)

                        value = parse_stack.pop()
                        key = parse_stack.pop()

                        if token_type == JSON_TYPE_STR:
                            return_dictionary[key] = str(value)

                        if token_type == JSON_TYPE_INT:
                            return_dictionary[key] = int(value)

                    # If this is the end of the object, then return ...
                    if char in ["}"]:
                        return return_dictionary

                    # .. otherwise, cleanup and continue
                    else:
                        token_type = JSON_TYPE_ERROR
                        token_sep = False
                        token_start = False

                # If this isn't anything interesting, assume it is part of a token
                if object_start and (
                    (char in ASCII_UPPERCASE) or (char in ASCII_DIGITS)
                ):
                    token_str = token_str + str(char)

        return return_dictionary

    def register_noun(self, noun: str, handler: APIBase) -> None:
        """Register a new object handler for the noun passed by the client.

        Parameters
        ----------

        noun: string
            String representing the noun to use in the API
        handler: APIBase
            Instance object handling the request from the client

        Raises
        ------

        KeyError:
            When the handler cannot be registered, or the `handler` is
            not a sub-class of [`APIBase`][urest.api.base.APIBase].

        """

        old_handler = APIBase()

        try:
            if noun in self._nouns:
                old_handler = self._nouns[noun]

            if isinstance(noun, str) and isinstance(handler, APIBase):
                self._nouns[noun.lower()] = handler

        except KeyError:
            if old_handler is not None:
                self._nouns[noun] = old_handler

    async def dispatch_noun(
        self,
        reader: asyncio.StreamReader,
        writer: asyncio.StreamWriter,
    ) -> None:
        """Core client handling routine. This connects the `reader` and
        `writer` streams from the IO library to the API requests detailed by
        the rest of the server. Most of the work is done elsewhere, by the API
        handlers: this is mostly a sanity check and a routing engine.

        !!! Danger
            This routine _must_ handle arbitrary network traffic, and so
            **must** be as defensive as possible to avoid security issues in
            the API layer which results from arbitrary input stuffing and
            alike. Assume that anything from the `reader` is potentially
            dangerous to the health of the API layer: unless shown otherwise...

        Parameters
        ----------

        reader: `asyncio.StreamReader`
            An asynchronous stream, representing the network response _from_ the
            client. This is usually set-up indirectly by the `asyncio` library as
            part of a network response to the client, and will be represented by
            an [`asyncio.StreamReader`](https://docs.python.org/3.4/library/asyncio-stream.html#streamreader).
        writer: `asyncio.StreamWriter`
            An asynchronous stream, representing the network response _to_ the
            client. This is usually set-up indirectly by the `asyncio` library as
            part of a network response to the client, and will be represented by
            an [`asyncio.StreamWriter`](https://docs.python.org/3.4/library/asyncio-stream.html#streamwriter).

        Raises
        ------

        IndexError:
            When an appropriate handler cannot be found for the noun.
        RESTClientError:
            When a handler exists, but cannot be used to service the request
            due to errors in the request from the client.

        """

        # Attempt the parse whatever rubbish the client sends, and assemble the
        # fragments into an API request. Any failures should result in an
        # `Exception`: success should result in an API call
        try:
            # Get the raw network request and decode into UTF-8
            request_uri = await asyncio.wait_for(reader.readline(), self.read_timeout)

            request_string = request_uri.decode("utf8")

            # Check for empty requests, and if found terminate the connection
            if request_string in [b"", b"\r\n"]:
                # DEBUG
                if __debug__:
                    print(
                        f"CLIENT: [{writer.get_extra_info('peername')[0]}] Empty request line",
                    )
                    return

            # DEBUG
            if __debug__:
                print(
                    f"CLIENT URI : [{writer.get_extra_info('peername')[0]}] {request_string.strip()}",
                )

            # Get the header of the request, if it is available, decoded into UTF-8
            request_header = {}
            request_line = None

            while request_line not in [b"", b"\r\n"]:
                request_line = await asyncio.wait_for(
                    reader.readline(),
                    self.read_timeout,
                )

                if request_line.find(b":") != -1:
                    name, value = request_line.split(b":", 1)
                    request_header[name.decode("utf-8").lower()] = value.decode(
                        "utf-8",
                    ).strip()

            # DEBUG
            if __debug__:
                print(
                    f"CLIENT HEAD: [{writer.get_extra_info('peername')[0]}] {request_header}",
                )

            # Check if there is a body to follow the header ...
            request_body = {}

            if "content-length" in request_header:
                request_length = int(request_header["content-length"])

                # ... check if there is _really a body to follow ...
                if request_length > 0:
                    # ... if so, get the rest of the body of the request, decoded into UTF-8

                    try:
                        request_length = int(request_header["content-length"])
                        request_data = await asyncio.wait_for(
                            reader.read(request_length),
                            self.read_timeout,
                        )
                        decoded_data = request_data.decode("utf8")
                        request_body = self._parse_data(decoded_data)
                    except IndexError as e:
                        # DEBUG
                        if __debug__:
                            print(f"!EXCEPTION!: {e}")
                            print(
                                f"!INVALID DATA!: [{writer.get_extra_info('peername')[0]}] {decoded_data}",
                            )
                        request_body = {}

                    # DEBUG
                    if __debug__:
                        print(
                            f"CLIENT DATA: [{writer.get_extra_info('peername')[0]}] {decoded_data}",
                        )
                        print(
                            f"CLIENT BODY: [{writer.get_extra_info('peername')[0]}] {request_body}",
                        )

                else:
                    # DEBUG
                    if __debug__:
                        print("CLIENT BODY: NONE")
            else:
                # DEBUG
                if __debug__:
                    print("CLIENT BODY: NONE")

            ## NOTE: Below is a somewhat long-winded approach to working out
            ##       the verb is based on the longest assumed verb:
            ##       '`DELETE`'. To avoid later parsing errors, and to
            ##       filter out the rubbish which might cause security
            ##       issues, we will search first for a 'space' within
            ##       the first six characters; then take either the first
            ##       six characters or the string up to the 'space'
            ##       whichever is shorter. These can then be compared
            ##       for sanity before we run the dispatcher

            # Work out the action we need to take ...

            request_string = request_uri.decode("utf8").strip()

            first_space = request_string.find(" ", 0, 7)

            if first_space > HTTP_LONGEST_VERB:
                first_space = HTTP_LONGEST_VERB

            verb = request_string[0:first_space].upper()

            # ... Work out the noun defining the class we need to use to resolve the
            # action ...

            uri_root = request_string.find("/", first_space)

            noun = ""
            start_noun = False

            for char in request_string[uri_root:]:
                if (
                    (char in ASCII_UPPERCASE)
                    or (char in ASCII_DIGITS)
                    or (char in ASCII_EXTRA)
                ):
                    start_noun = True
                    noun = noun + str(char)
                else:
                    if start_noun:
                        break

            # ... and then call the appropriate handler
            response = HTTPResponse()

            if verb == "DELETE":
                self._nouns[noun.lower()].delete_state()
                response.body = ""

            elif verb == "GET":
                return_state = self._nouns[noun.lower()].get_state()
                response_str = "{"

                try:
                    for key in return_state:
                        if isinstance(return_state[key], int):
                            response_str = (
                                response_str + f'"{key.lower()}": {return_state[key]},'
                            )
                        else:
                            response_str = (
                                response_str
                                + f'"{key.lower()}": "{return_state[key]}",'
                            )
                finally:
                    # Properly terminate the body
                    response_str = response_str[:-1] + "}"

                response.body = response_str

            elif verb == "POST":
                self._nouns[noun.lower()].set_state(request_body)
                response.body = ""

            elif verb == "PUT":
                self._nouns[noun.lower()].set_state(request_body)
                response.body = ""

            else:
                # Clearly not one of ours
                response.body = (
                    "<http><body><p>Invalid Method in Request</p></body></http>"
                )
                response.status = HTTPStatus.NOT_OK

            await response.send(writer)

            writer.write(b"\r\n")

            await writer.drain()

        # Deal with any exceptions. These are mostly client errors, and since the
        # REST API _should_ be idempotent, the client _should_ be able to simply
        # retry. So we won't do anything very fancy here
        except asyncio.TimeoutError:
            pass
        except Exception as e:
            if e.args[0] == errno.ECONNRESET:  # connection reset by client
                pass
            else:
                if hasattr(e, "message"):
                    raise RESTClientError(e.message) from None  # type: ignore
                else:
                    msg = "Unknown client Error"
                    raise RESTClientError(msg) from None

            # DEBUG
            if __debug__:
                print(f"!EXCEPTION!: {e}")

            response.body = "<http><body><p>Invalid Request</p></body></http>"
            response.status = HTTPStatus.NOT_OK

        # In principle the response should have been sent back to the client by now.
        # But we will give it one last try, and then also try to close the
        # connection cleanly for the client. This may not work due to the earlier
        # exceptions: but we will try anyway
        finally:
            # Do a soft close, dropping our end of the connection
            # to see if the client closes ...
            await writer.drain()
            writer.close()

            # ... if the client doesn't take the hint, wait
            # for `write_timeout` seconds and then force the close
            await asyncio.sleep(self.write_timeout)
            await writer.wait_closed()

    async def start(self) -> None:
        """Attach the method [`RESTServer.dispatch_noun()`]
        [urest.http.server.RESTServer.dispatch_noun] to an `asyncio` event
        loop, allowing the [`RESTServer.dispatch_noun()`]
        [urest.http.server.RESTServer.dispatch_noun] method to handle tasks
        representing network events from the client.

        Most of the implementation of this method is handled by [`asyncio.start_server`](https://docs.python.org/3.4/library/asyncio-stream.html# asyncio.start_server).
        In particular the
        [`asyncio.start_server`](https://docs.python.org/3.4/library/asyncio-stream.html# asyncio.start_server)
        method is responsible for setting up the lower-level networks socket,
        using the `host` and `port` class attributes holding the server (local)
        elements of the TCP/IP tuple. Lower level timers and queues are also
        provided by the resolution of the
        [`asyncio.start_server`](https://docs.python.org/3.4/library/asyncio-stream.html# asyncio.start_server)
        method, with the class attribute `backlog` being used to set the client
        (downstream) timeout.
        """

        # DEBUG
        if __debug__:
            print(f"SERVER: Started on {self.host}:{self.port}")

        self._server = await asyncio.start_server(
            self.dispatch_noun,
            host=self.host,
            port=self.port,
            backlog=self.backlog,
        )  # type: ignore

    async def stop(self) -> None:
        """Remove the tasks from an event loop, in preparation for the
        termination of that loop.

        Most of the implementation of this method is handled by the
        [`close`](https://docs.python.org/3.4/library/asyncio-protocol.html#asyncio.BaseTransport.close)
        method of `asyncio.BaseTransport`.
        """

        if self._server is not None:
            self._server.close()
            await self._server.wait_closed()
            self._server = None

            # DEBUG
            if __debug__:
                print("SERVER: Stopped")
        else:
            # DEBUG
            if __debug__:
                print("SERVER: Not started")

Functions

__init__
__init__(
    host: str = "0.0.0.0",
    port: int = 80,
    backlog: int = 5,
    read_timeout: int = 30,
    write_timeout: int = 5,
) -> None

Create an instance of the RESTServer class to handle client requests. In most cases there should only be once instance of RESTServer in each application (i.e. RESTServer should be treated as a singleton).

Parameters:
  • host (str, default: '0.0.0.0' ) –

    A resolvable DNS host name or IP address. Note that the exact requirements are determined by the asyncio.BaseEventLoop.create_server method, which should be checked carefully for implementation defined limitations.

    Default: An IPv4 sock on the local host.

  • port (int, default: 80 ) –

    The local (server) port to bind the socket to. Note that the exact requirements are determined by the asyncio.BaseEventLoop.create_server method, which should be checked carefully for implementation defined limitations (e.g. extra privileges required for system ports).

    Default: The IANA Assigned port 80 for an HTTP Server.

  • backlog (int, default: 5 ) –

    Roughly the size of the pool of connections for the underlying socket. Once this value has been exceeded, the tasks will be suspended by the co-routine handler until the underlying socket can clear them. Note that the size (and interpretation) of this value is system dependent: see the socket API for more details.

    Default: 5 (typically the maximum pool size allowed).

  • read_timeout (int, default: 30 ) –

    Length of time in seconds to wait for a response from the client before declaring failure.

    Default: 30 seconds.

  • write_timeout (int, default: 5 ) –

    Length of time in seconds to wait for the network socket to accept a write to the client, before declaring failure.

    Default: 5 seconds.

Source code in urest/http/server.py
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
def __init__(
    self,
    host: str = "0.0.0.0",
    port: int = 80,
    backlog: int = 5,
    read_timeout: int = 30,
    write_timeout: int = 5,
) -> None:
    """Create an instance of the `RESTServer` class to handle client
    requests. In most cases there should only be once instance of
    `RESTServer` in each application (i.e. `RESTServer` should be treated
    as a singleton).

    Parameters
    ----------

    host: string
        A resolvable DNS host name or IP address. Note that the exact
        requirements are determined by the
        [`asyncio.BaseEventLoop.create_server`](https://docs.python.org/3.4/
        library/asyncio-eventloop.html#asyncio.BaseEventLoop.create_server)
        method, which should be checked carefully for implementation defined
        limitations.

        **Default:** An IPv4 sock on the local host.
    port: integer
        The local (server) port to bind the socket to. Note that the exact
        requirements are determined by the [`asyncio.BaseEventLoop.create_server`](https://docs.python.org/3.4/library/asyncio-eventloop.html#asyncio.BaseEventLoop.create_server)
        method, which should be checked carefully for implementation defined
        limitations (e.g. extra privileges required for system ports).

        **Default:** The IANA Assigned port 80 for an HTTP Server.
    backlog: integer
        Roughly the size of the pool of connections for the underlying
        `socket`. Once this value has been exceeded, the tasks will be
        suspended by the co-routine handler until the underlying `socket` can
        clear them. Note that the size (and interpretation) of this value is
        system dependent: see the [`socket` API](https://docs.python.org/3.4/library/socket.html#module-socket)
        for more details.

        **Default:** 5 (typically the maximum pool size allowed).
    read_timeout: integer
        Length of time in seconds to wait for a response from the client before declaring
        failure.

        **Default:** 30 seconds.
    write_timeout: integer
        Length of time in seconds to wait for the network socket to accept a write to the
        client, before declaring failure.

        **Default:** 5 seconds.

    """
    self.host = host
    self.port = port
    self.backlog = backlog
    self.read_timeout = read_timeout
    self.write_timeout = write_timeout
    self._server = None
    self._nouns = {"": APIBase()}
dispatch_noun async
dispatch_noun(
    reader: asyncio.StreamReader,
    writer: asyncio.StreamWriter,
) -> None

Core client handling routine. This connects the reader and writer streams from the IO library to the API requests detailed by the rest of the server. Most of the work is done elsewhere, by the API handlers: this is mostly a sanity check and a routing engine.

Danger

This routine must handle arbitrary network traffic, and so must be as defensive as possible to avoid security issues in the API layer which results from arbitrary input stuffing and alike. Assume that anything from the reader is potentially dangerous to the health of the API layer: unless shown otherwise…

Parameters:
  • reader (StreamReader) –

    An asynchronous stream, representing the network response from the client. This is usually set-up indirectly by the asyncio library as part of a network response to the client, and will be represented by an asyncio.StreamReader.

  • writer (StreamWriter) –

    An asynchronous stream, representing the network response to the client. This is usually set-up indirectly by the asyncio library as part of a network response to the client, and will be represented by an asyncio.StreamWriter.

Raises:
  • IndexError:

    When an appropriate handler cannot be found for the noun.

  • RESTClientError:

    When a handler exists, but cannot be used to service the request due to errors in the request from the client.

Source code in urest/http/server.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
async def dispatch_noun(
    self,
    reader: asyncio.StreamReader,
    writer: asyncio.StreamWriter,
) -> None:
    """Core client handling routine. This connects the `reader` and
    `writer` streams from the IO library to the API requests detailed by
    the rest of the server. Most of the work is done elsewhere, by the API
    handlers: this is mostly a sanity check and a routing engine.

    !!! Danger
        This routine _must_ handle arbitrary network traffic, and so
        **must** be as defensive as possible to avoid security issues in
        the API layer which results from arbitrary input stuffing and
        alike. Assume that anything from the `reader` is potentially
        dangerous to the health of the API layer: unless shown otherwise...

    Parameters
    ----------

    reader: `asyncio.StreamReader`
        An asynchronous stream, representing the network response _from_ the
        client. This is usually set-up indirectly by the `asyncio` library as
        part of a network response to the client, and will be represented by
        an [`asyncio.StreamReader`](https://docs.python.org/3.4/library/asyncio-stream.html#streamreader).
    writer: `asyncio.StreamWriter`
        An asynchronous stream, representing the network response _to_ the
        client. This is usually set-up indirectly by the `asyncio` library as
        part of a network response to the client, and will be represented by
        an [`asyncio.StreamWriter`](https://docs.python.org/3.4/library/asyncio-stream.html#streamwriter).

    Raises
    ------

    IndexError:
        When an appropriate handler cannot be found for the noun.
    RESTClientError:
        When a handler exists, but cannot be used to service the request
        due to errors in the request from the client.

    """

    # Attempt the parse whatever rubbish the client sends, and assemble the
    # fragments into an API request. Any failures should result in an
    # `Exception`: success should result in an API call
    try:
        # Get the raw network request and decode into UTF-8
        request_uri = await asyncio.wait_for(reader.readline(), self.read_timeout)

        request_string = request_uri.decode("utf8")

        # Check for empty requests, and if found terminate the connection
        if request_string in [b"", b"\r\n"]:
            # DEBUG
            if __debug__:
                print(
                    f"CLIENT: [{writer.get_extra_info('peername')[0]}] Empty request line",
                )
                return

        # DEBUG
        if __debug__:
            print(
                f"CLIENT URI : [{writer.get_extra_info('peername')[0]}] {request_string.strip()}",
            )

        # Get the header of the request, if it is available, decoded into UTF-8
        request_header = {}
        request_line = None

        while request_line not in [b"", b"\r\n"]:
            request_line = await asyncio.wait_for(
                reader.readline(),
                self.read_timeout,
            )

            if request_line.find(b":") != -1:
                name, value = request_line.split(b":", 1)
                request_header[name.decode("utf-8").lower()] = value.decode(
                    "utf-8",
                ).strip()

        # DEBUG
        if __debug__:
            print(
                f"CLIENT HEAD: [{writer.get_extra_info('peername')[0]}] {request_header}",
            )

        # Check if there is a body to follow the header ...
        request_body = {}

        if "content-length" in request_header:
            request_length = int(request_header["content-length"])

            # ... check if there is _really a body to follow ...
            if request_length > 0:
                # ... if so, get the rest of the body of the request, decoded into UTF-8

                try:
                    request_length = int(request_header["content-length"])
                    request_data = await asyncio.wait_for(
                        reader.read(request_length),
                        self.read_timeout,
                    )
                    decoded_data = request_data.decode("utf8")
                    request_body = self._parse_data(decoded_data)
                except IndexError as e:
                    # DEBUG
                    if __debug__:
                        print(f"!EXCEPTION!: {e}")
                        print(
                            f"!INVALID DATA!: [{writer.get_extra_info('peername')[0]}] {decoded_data}",
                        )
                    request_body = {}

                # DEBUG
                if __debug__:
                    print(
                        f"CLIENT DATA: [{writer.get_extra_info('peername')[0]}] {decoded_data}",
                    )
                    print(
                        f"CLIENT BODY: [{writer.get_extra_info('peername')[0]}] {request_body}",
                    )

            else:
                # DEBUG
                if __debug__:
                    print("CLIENT BODY: NONE")
        else:
            # DEBUG
            if __debug__:
                print("CLIENT BODY: NONE")

        ## NOTE: Below is a somewhat long-winded approach to working out
        ##       the verb is based on the longest assumed verb:
        ##       '`DELETE`'. To avoid later parsing errors, and to
        ##       filter out the rubbish which might cause security
        ##       issues, we will search first for a 'space' within
        ##       the first six characters; then take either the first
        ##       six characters or the string up to the 'space'
        ##       whichever is shorter. These can then be compared
        ##       for sanity before we run the dispatcher

        # Work out the action we need to take ...

        request_string = request_uri.decode("utf8").strip()

        first_space = request_string.find(" ", 0, 7)

        if first_space > HTTP_LONGEST_VERB:
            first_space = HTTP_LONGEST_VERB

        verb = request_string[0:first_space].upper()

        # ... Work out the noun defining the class we need to use to resolve the
        # action ...

        uri_root = request_string.find("/", first_space)

        noun = ""
        start_noun = False

        for char in request_string[uri_root:]:
            if (
                (char in ASCII_UPPERCASE)
                or (char in ASCII_DIGITS)
                or (char in ASCII_EXTRA)
            ):
                start_noun = True
                noun = noun + str(char)
            else:
                if start_noun:
                    break

        # ... and then call the appropriate handler
        response = HTTPResponse()

        if verb == "DELETE":
            self._nouns[noun.lower()].delete_state()
            response.body = ""

        elif verb == "GET":
            return_state = self._nouns[noun.lower()].get_state()
            response_str = "{"

            try:
                for key in return_state:
                    if isinstance(return_state[key], int):
                        response_str = (
                            response_str + f'"{key.lower()}": {return_state[key]},'
                        )
                    else:
                        response_str = (
                            response_str
                            + f'"{key.lower()}": "{return_state[key]}",'
                        )
            finally:
                # Properly terminate the body
                response_str = response_str[:-1] + "}"

            response.body = response_str

        elif verb == "POST":
            self._nouns[noun.lower()].set_state(request_body)
            response.body = ""

        elif verb == "PUT":
            self._nouns[noun.lower()].set_state(request_body)
            response.body = ""

        else:
            # Clearly not one of ours
            response.body = (
                "<http><body><p>Invalid Method in Request</p></body></http>"
            )
            response.status = HTTPStatus.NOT_OK

        await response.send(writer)

        writer.write(b"\r\n")

        await writer.drain()

    # Deal with any exceptions. These are mostly client errors, and since the
    # REST API _should_ be idempotent, the client _should_ be able to simply
    # retry. So we won't do anything very fancy here
    except asyncio.TimeoutError:
        pass
    except Exception as e:
        if e.args[0] == errno.ECONNRESET:  # connection reset by client
            pass
        else:
            if hasattr(e, "message"):
                raise RESTClientError(e.message) from None  # type: ignore
            else:
                msg = "Unknown client Error"
                raise RESTClientError(msg) from None

        # DEBUG
        if __debug__:
            print(f"!EXCEPTION!: {e}")

        response.body = "<http><body><p>Invalid Request</p></body></http>"
        response.status = HTTPStatus.NOT_OK

    # In principle the response should have been sent back to the client by now.
    # But we will give it one last try, and then also try to close the
    # connection cleanly for the client. This may not work due to the earlier
    # exceptions: but we will try anyway
    finally:
        # Do a soft close, dropping our end of the connection
        # to see if the client closes ...
        await writer.drain()
        writer.close()

        # ... if the client doesn't take the hint, wait
        # for `write_timeout` seconds and then force the close
        await asyncio.sleep(self.write_timeout)
        await writer.wait_closed()
register_noun
register_noun(noun: str, handler: APIBase) -> None

Register a new object handler for the noun passed by the client.

Parameters:
  • noun (str) –

    String representing the noun to use in the API

  • handler (APIBase) –

    Instance object handling the request from the client

Raises:
  • KeyError:

    When the handler cannot be registered, or the handler is not a sub-class of APIBase.

Source code in urest/http/server.py
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
def register_noun(self, noun: str, handler: APIBase) -> None:
    """Register a new object handler for the noun passed by the client.

    Parameters
    ----------

    noun: string
        String representing the noun to use in the API
    handler: APIBase
        Instance object handling the request from the client

    Raises
    ------

    KeyError:
        When the handler cannot be registered, or the `handler` is
        not a sub-class of [`APIBase`][urest.api.base.APIBase].

    """

    old_handler = APIBase()

    try:
        if noun in self._nouns:
            old_handler = self._nouns[noun]

        if isinstance(noun, str) and isinstance(handler, APIBase):
            self._nouns[noun.lower()] = handler

    except KeyError:
        if old_handler is not None:
            self._nouns[noun] = old_handler
start async
start() -> None

Attach the method RESTServer.dispatch_noun() to an asyncio event loop, allowing the RESTServer.dispatch_noun() method to handle tasks representing network events from the client.

Most of the implementation of this method is handled by asyncio.start_server. In particular the asyncio.start_server method is responsible for setting up the lower-level networks socket, using the host and port class attributes holding the server (local) elements of the TCP/IP tuple. Lower level timers and queues are also provided by the resolution of the asyncio.start_server method, with the class attribute backlog being used to set the client (downstream) timeout.

Source code in urest/http/server.py
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
async def start(self) -> None:
    """Attach the method [`RESTServer.dispatch_noun()`]
    [urest.http.server.RESTServer.dispatch_noun] to an `asyncio` event
    loop, allowing the [`RESTServer.dispatch_noun()`]
    [urest.http.server.RESTServer.dispatch_noun] method to handle tasks
    representing network events from the client.

    Most of the implementation of this method is handled by [`asyncio.start_server`](https://docs.python.org/3.4/library/asyncio-stream.html# asyncio.start_server).
    In particular the
    [`asyncio.start_server`](https://docs.python.org/3.4/library/asyncio-stream.html# asyncio.start_server)
    method is responsible for setting up the lower-level networks socket,
    using the `host` and `port` class attributes holding the server (local)
    elements of the TCP/IP tuple. Lower level timers and queues are also
    provided by the resolution of the
    [`asyncio.start_server`](https://docs.python.org/3.4/library/asyncio-stream.html# asyncio.start_server)
    method, with the class attribute `backlog` being used to set the client
    (downstream) timeout.
    """

    # DEBUG
    if __debug__:
        print(f"SERVER: Started on {self.host}:{self.port}")

    self._server = await asyncio.start_server(
        self.dispatch_noun,
        host=self.host,
        port=self.port,
        backlog=self.backlog,
    )  # type: ignore
stop async
stop() -> None

Remove the tasks from an event loop, in preparation for the termination of that loop.

Most of the implementation of this method is handled by the close method of asyncio.BaseTransport.

Source code in urest/http/server.py
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
async def stop(self) -> None:
    """Remove the tasks from an event loop, in preparation for the
    termination of that loop.

    Most of the implementation of this method is handled by the
    [`close`](https://docs.python.org/3.4/library/asyncio-protocol.html#asyncio.BaseTransport.close)
    method of `asyncio.BaseTransport`.
    """

    if self._server is not None:
        self._server.close()
        await self._server.wait_closed()
        self._server = None

        # DEBUG
        if __debug__:
            print("SERVER: Stopped")
    else:
        # DEBUG
        if __debug__:
            print("SERVER: Not started")

urest.http.HTTPResponse

Create a response object, representing the raw HTTP header returned to the network client.

This instance is guaranteed to be a valid instance on creation, and should also be a valid HTTP response. However the caller should check the validity of the header before returning to the client. In particular, responses returned to the client by this class must be formatted according to the HTTP/1.1 specification and must be valid.

Attributes:
  • body (string) –

    The raw HTTP body returned to the client. This is Empty by default as the return string is usually built by the caller via the getters and setters of HTTPResponse.

  • status (HTTPStatus) –

    HTTP status code, which must be formed from the set HTTPResponse. Arbitrary return codes are not supported by this class.

  • mimetype (string) –

    A valid HTTP mime type. This is None by default and should be set once the body of the HTTPResponse has been created.

  • close (bool) –

    When set True the connection to the client will be closed by the RESTServer once the HTTPResponse has been sent. Otherwise, when set to False this will flag to the client that the created HTTPResponse is part of a sequence to be sent over the same connection.

  • header (Optional[dict[str, str]]) –

    Raw (key, value) pairs for HTTP response header fields. This allows setting of arbitrary fields by the caller, without extending or sub-classing HTTPResponse.

Source code in urest/http/response.py
 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
class HTTPResponse:
    """Create a response object, representing the raw HTTP header returned to
    the network client.

    This instance is guaranteed to be a valid _instance_ on creation, and _should_
    also be a valid HTTP response. However the caller should check the validity of
    the header before returning to the client. In particular,  responses returned
    to the client by this class _must_ be formatted according to the [HTTP/1.1
    specification](https://www.ietf.org/rfc/rfc2616.txt) and _must_ be valid.

    Attributes
    ----------

    body: string
        The raw HTTP body returned to the client. This is `Empty` by default
        as the return string is usually built by the caller via the `getters`
        and `setters` of [`HTTPResponse`][urest.http.response.HTTPResponse].
    status: urest.http.response.HTTPStatus
        HTTP status code, which must be formed from the set [`HTTPResponse`]
        [urest.http.response.HTTPResponse]. Arbitrary return codes are **not**
        supported by this class.
    mimetype: string
        A valid HTTP mime type. This is `None` by default and should be set
        once the `body` of the [`HTTPResponse`][urest.http.response.HTTPResponse] has been
        created.
    close: bool
        When set `True` the connection to the client will be closed by the
        [`RESTServer`][urest.http.server.RESTServer] once the
        [`HTTPResponse`][urest.http.response.HTTPResponse] has been sent. Otherwise, when set
        to `False` this will flag to the client that the created
        [`HTTPResponse`][urest.http.response.HTTPResponse] is part of a sequence to be sent
        over the same connection.
    header:  Optional[dict[str, str]]
        Raw (key, value) pairs for HTTP response header fields. This allows
        setting of arbitrary fields by the caller, without extending or
        sub-classing [`HTTPResponse`][urest.http.response.HTTPResponse].

    """

    ##
    ## Attributes
    ##

    _body: str
    _status: HTTPStatus
    _mimetype: Optional[str]
    _close: bool
    _header: Optional[dict[str, str]]

    ##
    ## Constructor
    ##

    def __init__(
        self,
        body: str = "",
        status: HTTPStatus = HTTPStatus.OK,
        mimetype: Optional[str] = None,
        close: bool = True,
        header: Optional[dict[str, str]] = None,
    ) -> None:
        """Encode a suitable HTTP response to send to the network client.

        Parameters
        ----------

        body: string
            The raw HTTP body returned to the client. This is `Empty` by default
            as the return string is usually built by the caller via the `getters`
            and `setters` of [`HTTPResponse`][urest.http.response.HTTPResponse].
        status: urest.http.response.HTTPStatus
            HTTP status code, which must be formed from the set [`HTTPResponse`]
            [urest.http.response.HTTPResponse]. Arbitrary return codes are **not**
            supported by this class.
        mimetype: string
            A valid HTTP mime type. This is `None` by default and should be set
            once the `body` of the [`HTTPResponse`][urest.http.response.HTTPResponse] has been
            created.
        close: bool
            When set `True` the connection to the client will be closed by the
            [`RESTServer`][urest.http.server.RESTServer] once the
            [`HTTPResponse`][urest.http.response.HTTPResponse] has been sent. Otherwise, when set
            to `False` this will flag to the client that the created
            [`HTTPResponse`][urest.http.response.HTTPResponse] is part of a sequence to be sent
            over the same connection.
        header:  Optional[dict[str, str]]
            Raw (key, value) pairs for HTTP response header fields. This allows
            setting of arbitrary fields by the caller, without extending or
            sub-classing [`HTTPResponse`][urest.http.response.HTTPResponse].

        """

        if status in HTTPStatus:
            self._status = status
        else:
            msg = "Invalid HTTP status code passed to the HTTP Response class"
            raise ValueError(msg)

        if body is not None and isinstance(body, str):
            self._body = body
        else:
            self._body = ""

        self._mimetype = mimetype
        self._close = close

        if header is None:
            self._header = {}
        else:
            self._header = header

    ##
    ## Getters and Setters
    ##

    # HTTP Body

    @property
    def body(self) -> str:
        """The raw HTTP response, formatted to return to the client as the HTTP
        response."""

        return self._body

    @body.setter
    def body(self, new_body: str) -> None:
        if new_body is not None and isinstance(new_body, str):
            self._body = new_body
        else:
            self._body = ""

    # HTTP Status

    @property
    def status(self) -> HTTPStatus:
        """A valid [`HTTPStatus`][urest.http.response.HTTPStatus] representing
        the current error/status code that will be returned to the client."""
        return self._status

    @status.setter
    def status(self, new_status: HTTPStatus) -> None:
        if new_status in HTTPStatus:
            self._status = new_status
        else:
            msg = "Invalid HTTP status code passed to the HTTP Response class"
            raise ValueError(msg)

    ##
    ## Functions
    ##

    async def send(self, writer: asyncio.StreamWriter) -> None:
        """Send an appropriate response to the client, based on the status
        code.

        This method assembles the full HTTP 1.1 header, based on the `mimetype`
        the content currently in the `body`, and the error code forming the
        `status` of the response to the client.

        !!! Note
             The the actual sending of this HTTP 1.1 header to the client
             is the responsibility of the caller. This function only assists in
             correctly forming that response

        Parameters
        ----------

        writer: `asyncio.StreamWriter`
            An asynchronous stream, representing the network response to the
            client. This is usually set-up indirectly by the caller as part of a network
            response to the client. As such is is usually just a pass-though from the
            dispatch call of the server. For an example see the dispatcher
            [`RESTServer.dispatch_noun()`][urest.http.server.RESTServer.dispatch_noun].

        Returns
        -------

        `async`

            The return type is complex, and indicates this method is expected to
            be run as a co-routine under the `asyncio` library.

        """

        # **NOTE**: This implementation should be in "match/case", but MicroPython
        #       doesn't have a 3.10 release yet. When it does, this
        #       implementation should be updated

        if self._status == HTTPStatus.OK:
            # First tell the client we accepted the request
            writer.write(b"HTTP/1.1 200 OK\r\n")

            # Then we try to assemble the body
            if self._mimetype is not None:
                writer.write(f"Content-Type: {self._mimetype}\r\n".encode())

        elif self._status == HTTPStatus.NOT_OK:
            # Tell the client we think we can route it: but the request
            # makes no sense
            writer.write(b"HTTP/1.1 400 Bad Request\r\n")

        elif self._status == HTTPStatus.NOT_FOUND:
            # Tell the client we can't route their request
            writer.write(b"HTTP/1.1 404 Not Found\r\n")

        else:
            # This _really_ shouldn't be here. Assume an internal error
            writer.write(b"HTTP/1.1 500 Internal Server Error\r\n")

        # Send the body length
        writer.write(f"Content-Length: {len(self._body)}\r\n".encode())

        # Send the body content type
        writer.write(b"Content-Type: text/html\r\n")

        # Send any other header fields
        if self._header is not None and len(self._header) > 0:
            for key, value in self._header.items():
                writer.write(f"{key}: {value}\r\n".encode())

        # Send the HTTP connection state
        if self._close:
            writer.write(b"Connection: close\r\n")
        else:
            writer.write(b"Connection: keep-alive\r\n")

        # Send the body itself...
        writer.write(f"\r\n{self._body}\r\n".encode())

        # ... and ensure that it gets back to the client
        await writer.drain()

Attributes

body property writable
body: str

The raw HTTP response, formatted to return to the client as the HTTP response.

status property writable
status: HTTPStatus

A valid HTTPStatus representing the current error/status code that will be returned to the client.

Functions

__init__
__init__(
    body: str = "",
    status: HTTPStatus = HTTPStatus.OK,
    mimetype: Optional[str] = None,
    close: bool = True,
    header: Optional[dict[str, str]] = None,
) -> None

Encode a suitable HTTP response to send to the network client.

Parameters:
  • body (str, default: '' ) –

    The raw HTTP body returned to the client. This is Empty by default as the return string is usually built by the caller via the getters and setters of HTTPResponse.

  • status (HTTPStatus, default: OK ) –

    HTTP status code, which must be formed from the set HTTPResponse. Arbitrary return codes are not supported by this class.

  • mimetype (Optional[str], default: None ) –

    A valid HTTP mime type. This is None by default and should be set once the body of the HTTPResponse has been created.

  • close (bool, default: True ) –

    When set True the connection to the client will be closed by the RESTServer once the HTTPResponse has been sent. Otherwise, when set to False this will flag to the client that the created HTTPResponse is part of a sequence to be sent over the same connection.

  • header (Optional[dict[str, str]], default: None ) –

    Raw (key, value) pairs for HTTP response header fields. This allows setting of arbitrary fields by the caller, without extending or sub-classing HTTPResponse.

Source code in urest/http/response.py
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
def __init__(
    self,
    body: str = "",
    status: HTTPStatus = HTTPStatus.OK,
    mimetype: Optional[str] = None,
    close: bool = True,
    header: Optional[dict[str, str]] = None,
) -> None:
    """Encode a suitable HTTP response to send to the network client.

    Parameters
    ----------

    body: string
        The raw HTTP body returned to the client. This is `Empty` by default
        as the return string is usually built by the caller via the `getters`
        and `setters` of [`HTTPResponse`][urest.http.response.HTTPResponse].
    status: urest.http.response.HTTPStatus
        HTTP status code, which must be formed from the set [`HTTPResponse`]
        [urest.http.response.HTTPResponse]. Arbitrary return codes are **not**
        supported by this class.
    mimetype: string
        A valid HTTP mime type. This is `None` by default and should be set
        once the `body` of the [`HTTPResponse`][urest.http.response.HTTPResponse] has been
        created.
    close: bool
        When set `True` the connection to the client will be closed by the
        [`RESTServer`][urest.http.server.RESTServer] once the
        [`HTTPResponse`][urest.http.response.HTTPResponse] has been sent. Otherwise, when set
        to `False` this will flag to the client that the created
        [`HTTPResponse`][urest.http.response.HTTPResponse] is part of a sequence to be sent
        over the same connection.
    header:  Optional[dict[str, str]]
        Raw (key, value) pairs for HTTP response header fields. This allows
        setting of arbitrary fields by the caller, without extending or
        sub-classing [`HTTPResponse`][urest.http.response.HTTPResponse].

    """

    if status in HTTPStatus:
        self._status = status
    else:
        msg = "Invalid HTTP status code passed to the HTTP Response class"
        raise ValueError(msg)

    if body is not None and isinstance(body, str):
        self._body = body
    else:
        self._body = ""

    self._mimetype = mimetype
    self._close = close

    if header is None:
        self._header = {}
    else:
        self._header = header
send async
send(writer: asyncio.StreamWriter) -> None

Send an appropriate response to the client, based on the status code.

This method assembles the full HTTP 1.1 header, based on the mimetype the content currently in the body, and the error code forming the status of the response to the client.

Note

The the actual sending of this HTTP 1.1 header to the client is the responsibility of the caller. This function only assists in correctly forming that response

Parameters:
  • writer (StreamWriter) –

    An asynchronous stream, representing the network response to the client. This is usually set-up indirectly by the caller as part of a network response to the client. As such is is usually just a pass-though from the dispatch call of the server. For an example see the dispatcher RESTServer.dispatch_noun().

Returns:
  • `async`

    The return type is complex, and indicates this method is expected to be run as a co-routine under the asyncio library.

Source code in urest/http/response.py
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
async def send(self, writer: asyncio.StreamWriter) -> None:
    """Send an appropriate response to the client, based on the status
    code.

    This method assembles the full HTTP 1.1 header, based on the `mimetype`
    the content currently in the `body`, and the error code forming the
    `status` of the response to the client.

    !!! Note
         The the actual sending of this HTTP 1.1 header to the client
         is the responsibility of the caller. This function only assists in
         correctly forming that response

    Parameters
    ----------

    writer: `asyncio.StreamWriter`
        An asynchronous stream, representing the network response to the
        client. This is usually set-up indirectly by the caller as part of a network
        response to the client. As such is is usually just a pass-though from the
        dispatch call of the server. For an example see the dispatcher
        [`RESTServer.dispatch_noun()`][urest.http.server.RESTServer.dispatch_noun].

    Returns
    -------

    `async`

        The return type is complex, and indicates this method is expected to
        be run as a co-routine under the `asyncio` library.

    """

    # **NOTE**: This implementation should be in "match/case", but MicroPython
    #       doesn't have a 3.10 release yet. When it does, this
    #       implementation should be updated

    if self._status == HTTPStatus.OK:
        # First tell the client we accepted the request
        writer.write(b"HTTP/1.1 200 OK\r\n")

        # Then we try to assemble the body
        if self._mimetype is not None:
            writer.write(f"Content-Type: {self._mimetype}\r\n".encode())

    elif self._status == HTTPStatus.NOT_OK:
        # Tell the client we think we can route it: but the request
        # makes no sense
        writer.write(b"HTTP/1.1 400 Bad Request\r\n")

    elif self._status == HTTPStatus.NOT_FOUND:
        # Tell the client we can't route their request
        writer.write(b"HTTP/1.1 404 Not Found\r\n")

    else:
        # This _really_ shouldn't be here. Assume an internal error
        writer.write(b"HTTP/1.1 500 Internal Server Error\r\n")

    # Send the body length
    writer.write(f"Content-Length: {len(self._body)}\r\n".encode())

    # Send the body content type
    writer.write(b"Content-Type: text/html\r\n")

    # Send any other header fields
    if self._header is not None and len(self._header) > 0:
        for key, value in self._header.items():
            writer.write(f"{key}: {value}\r\n".encode())

    # Send the HTTP connection state
    if self._close:
        writer.write(b"Connection: close\r\n")
    else:
        writer.write(b"Connection: keep-alive\r\n")

    # Send the body itself...
    writer.write(f"\r\n{self._body}\r\n".encode())

    # ... and ensure that it gets back to the client
    await writer.drain()