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.
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 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 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 |
|
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: |
|
---|
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 |
|
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: |
|
---|
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 |
|
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: |
|
---|
Raises: |
|
---|
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 |
|
register_noun
register_noun(noun: str, handler: APIBase) -> None
Register a new object handler for the noun passed by the client.
Parameters: |
|
---|
Raises: |
|
---|
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 |
|
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 |
|
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 |
|
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: |
|
---|
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 |
|
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: |
|
---|
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 |
|
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: |
|
---|
Returns: |
|
---|
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 |
|