15 | {% endfor %}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/control-components-from-web-server/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | import network
3 |
4 | # Replace the following with your WIFI Credentials
5 | SSID = ""
6 | SSI_PASSWORD = ""
7 |
8 | def do_connect():
9 | import network
10 | sta_if = network.WLAN(network.STA_IF)
11 | if not sta_if.isconnected():
12 | print('connecting to network...')
13 | sta_if.active(True)
14 | sta_if.connect(SSID, SSI_PASSWORD)
15 | while not sta_if.isconnected():
16 | pass
17 | print('Connected! Network config:', sta_if.ifconfig())
18 |
19 | print("Connecting to your wifi...")
20 | do_connect()
--------------------------------------------------------------------------------
/websocket_using_microdot/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | import network
3 |
4 | # Replace the following with your WIFI Credentials
5 | SSID = ""
6 | SSI_PASSWORD = ""
7 |
8 | def do_connect():
9 | import network
10 | sta_if = network.WLAN(network.STA_IF)
11 | if not sta_if.isconnected():
12 | print('connecting to network...')
13 | sta_if.active(True)
14 | sta_if.connect(SSID, SSI_PASSWORD)
15 | while not sta_if.isconnected():
16 | pass
17 | print('Connected! Network config:', sta_if.ifconfig())
18 |
19 | print("Connecting to your wifi...")
20 | do_connect()
--------------------------------------------------------------------------------
/microdot-dynamic-component-path/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | import network
3 |
4 | # Replace the following with your WIFI Credentials
5 | SSID = ""
6 | SSI_PASSWORD = ""
7 |
8 | def do_connect():
9 | import network
10 | sta_if = network.WLAN(network.STA_IF)
11 | if not sta_if.isconnected():
12 | print('connecting to network...')
13 | sta_if.active(True)
14 | sta_if.connect(SSID, SSI_PASSWORD)
15 | while not sta_if.isconnected():
16 | pass
17 | print('Connected! Network config:', sta_if.ifconfig())
18 |
19 | print("Connecting to your wifi...")
20 | do_connect()
--------------------------------------------------------------------------------
/umqtt.simple/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | import network, utime, machine
3 |
4 | # Replace the following with your WIFI Credentials
5 | SSID = ""
6 | SSID_PASSWORD = ""
7 |
8 |
9 | def do_connect():
10 | sta_if = network.WLAN(network.STA_IF)
11 | if not sta_if.isconnected():
12 | print('connecting to network...')
13 | sta_if.active(True)
14 | sta_if.connect(SSID, SSID_PASSWORD)
15 | while not sta_if.isconnected():
16 | print("Attempting to connect....")
17 | utime.sleep(1)
18 | print('Connected! Network config:', sta_if.ifconfig())
19 |
20 | print("Connecting to your wifi...")
21 | do_connect()
22 |
--------------------------------------------------------------------------------
/mqtt-bme280-weather-station/boot.py:
--------------------------------------------------------------------------------
1 | # boot.py -- run on boot-up
2 | import network, utime, machine
3 |
4 | # Replace the following with your WIFI Credentials
5 | SSID = ""
6 | SSID_PASSWORD = ""
7 |
8 |
9 | def do_connect():
10 | sta_if = network.WLAN(network.STA_IF)
11 | if not sta_if.isconnected():
12 | print('connecting to network...')
13 | sta_if.active(True)
14 | sta_if.connect(SSID, SSID_PASSWORD)
15 | while not sta_if.isconnected():
16 | print("Attempting to connect....")
17 | utime.sleep(1)
18 | print('Connected! Network config:', sta_if.ifconfig())
19 |
20 | print("Connecting to your wifi...")
21 | do_connect()
--------------------------------------------------------------------------------
/mqtt-bme280-weather-station/bme_module.py:
--------------------------------------------------------------------------------
1 | import machine
2 | import bme280
3 | import math
4 |
5 | class BME280Module:
6 | SEA_LEVEL_PRESSURE_HPA = 1013.25
7 | def __init__(self, id, scl_pin, sda_pin):
8 | self.i2c = machine.I2C(id=id, scl=machine.Pin(scl_pin), sda=machine.Pin(sda_pin), freq=400000)
9 | self.bme = bme280.BME280(i2c=self.i2c)
10 |
11 | def get_sensor_readings(self):
12 | (temperature, pressure, humidity) = self.bme.values
13 | temperature_val = float(temperature[:len(temperature) - 1])
14 | humidity_val = float(humidity[:len(humidity) - 1])
15 | pressure_val = float(pressure[:len(pressure) - 3])
16 |
17 | # Altitude calculation
18 | altitude_val = 44330 * (1.0 - math.pow(pressure_val / BME280Module.SEA_LEVEL_PRESSURE_HPA, 0.1903))
19 |
20 | return (temperature_val, pressure_val, humidity_val, altitude_val)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/rgb-module/rgb_dim_brightness.py:
--------------------------------------------------------------------------------
1 | import time
2 | from machine import Pin,PWM
3 |
4 | # RGB
5 | RED = 0
6 | GREEN = 1
7 | BLUE = 2
8 |
9 | # Declare pins
10 | pwm_pins = [13,14,15]
11 | # Setup pins for PWM
12 | pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])),
13 | PWM(Pin(pwm_pins[BLUE]))]
14 | # Set pwm frequency
15 | [pwm.freq(1000) for pwm in pwms]
16 |
17 | def turn_off_rgb():
18 | pwms[RED].duty_u16(0)
19 | pwms[GREEN].duty_u16(0)
20 | pwms[BLUE].duty_u16(0)
21 | time.sleep(0.1)
22 |
23 | # Deinitialize PWM on all pins
24 | def deinit_pwm_pins():
25 | pwms[RED].deinit()
26 | pwms[GREEN].deinit()
27 | pwms[BLUE].deinit()
28 |
29 | # main function
30 | def main():
31 | while True:
32 | for pwm in pwms:
33 | # Turn off each RGB
34 | turn_off_rgb()
35 |
36 | for duty_value in range(0, 65535, 16):
37 | pwm.duty_u16(duty_value)
38 | time.sleep(0.001)
39 |
40 | if __name__ == "__main__":
41 | try:
42 | main()
43 | except KeyboardInterrupt:
44 | deinit_pwm_pins()
--------------------------------------------------------------------------------
/websocket_using_microdot/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | MicroPython WebSocket
8 |
9 |
10 |
11 |
12 |
13 |
MicroPython WebSocket
14 |
15 |
25 |
26 |
LDR/Photoresistor
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/rgb-module/basic_rgb_show.py:
--------------------------------------------------------------------------------
1 | import time
2 | from machine import Pin,PWM
3 |
4 | # RGB
5 | RED = 0
6 | GREEN = 1
7 | BLUE = 2
8 |
9 | # Declare pins
10 | pwm_pins = [13,14,15]
11 | # Setup pins for PWM
12 | pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])),
13 | PWM(Pin(pwm_pins[BLUE]))]
14 | # Set pwm frequency
15 | [pwm.freq(1000) for pwm in pwms]
16 |
17 | # Deinitialize PWM on all pins
18 | def deinit_pwm_pins():
19 | pwms[RED].deinit()
20 | pwms[GREEN].deinit()
21 | pwms[BLUE].deinit()
22 |
23 | # main function
24 | def main():
25 | while True:
26 | # Display RED
27 | pwms[RED].duty_u16(65535)
28 | pwms[GREEN].duty_u16(0)
29 | pwms[BLUE].duty_u16(0)
30 | time.sleep(1)
31 |
32 | # Display GREEN
33 | pwms[RED].duty_u16(0)
34 | pwms[GREEN].duty_u16(65535)
35 | pwms[BLUE].duty_u16(0)
36 | time.sleep(1)
37 |
38 | # Display BLUE
39 | pwms[RED].duty_u16(0)
40 | pwms[GREEN].duty_u16(0)
41 | pwms[BLUE].duty_u16(65535)
42 | time.sleep(1)
43 |
44 | if __name__ == "__main__":
45 | try:
46 | main()
47 | except KeyboardInterrupt:
48 | deinit_pwm_pins()
49 |
50 |
--------------------------------------------------------------------------------
/websocket_using_microdot/static/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-white: #fff;
3 | --color-dark-variant: #f3f5f8;
4 | --border-radius-1: 0.4rem;
5 | }
6 |
7 | * {
8 | margin: 0;
9 | padding: 0;
10 | outline: 0;
11 | }
12 |
13 | body {
14 | width: 100vw;
15 | height: 100vh;
16 | overflow-x: hidden;
17 | background: var(--color-dark-variant);
18 | }
19 | h1 {
20 | margin-top: 0.4rem;
21 | margin-left: 1.6rem;
22 | }
23 |
24 | .container {
25 | display: grid;
26 | width: 96%;
27 | margin: 0 auto;
28 | gap: 1.8rem;
29 | grid-template-columns: 14rem auto;
30 | }
31 | aside {
32 | margin-top: 1.4rem;
33 | height: 100vh;
34 | background: var(--color-white);
35 | border-radius: var(--border-radius-1);
36 | padding: 0.4rem;
37 | display: flex;
38 | flex-direction: column;
39 | align-items: center;
40 | justify-content: center;
41 | }
42 | .values {
43 | text-align: center;
44 | resize: none;
45 | height: 100%;
46 | font-size: 1.5rem;
47 | font-weight: bold;
48 | }
49 |
50 | main {
51 | margin-top: 1.4rem;
52 | background: var(--color-white);
53 | padding: 0.4rem;
54 | /* border: 1px solid red; */
55 | text-align: center;
56 | }
57 |
58 | .wrapper {
59 | display: flex;
60 | align-items: center;
61 | justify-content: center;
62 | }
63 |
--------------------------------------------------------------------------------
/websocket_using_microdot/main.py:
--------------------------------------------------------------------------------
1 | from microdot_asyncio import Microdot, Response, send_file
2 | from microdot_utemplate import render_template
3 | from microdot_asyncio_websocket import with_websocket
4 | from ldr_photoresistor_module import LDR
5 | import time
6 |
7 | # Initialize MicroDot
8 | app = Microdot()
9 | Response.default_content_type = 'text/html'
10 |
11 | # LDR module
12 | ldr = LDR(27)
13 |
14 | # root route
15 | @app.route('/')
16 | async def index(request):
17 | return render_template('index.html')
18 |
19 |
20 | @app.route('/ws')
21 | @with_websocket
22 | async def read_sensor(request, ws):
23 | while True:
24 | # data = await ws.receive()
25 | time.sleep(.1)
26 | await ws.send(str(ldr.get_light_percentage()))
27 |
28 | # Static CSS/JSS
29 | @app.route("/static/")
30 | def static(request, path):
31 | if ".." in path:
32 | # directory traversal is not allowed
33 | return "Not found", 404
34 | return send_file("static/" + path)
35 |
36 |
37 | # shutdown
38 | @app.get('/shutdown')
39 | def shutdown(request):
40 | request.app.shutdown()
41 | return 'The server is shutting down...'
42 |
43 |
44 | if __name__ == "__main__":
45 | try:
46 | app.run()
47 | except KeyboardInterrupt:
48 | pass
49 |
--------------------------------------------------------------------------------
/websocket_using_microdot/microdot_utemplate.py:
--------------------------------------------------------------------------------
1 | from utemplate import recompile
2 |
3 | _loader = None
4 |
5 |
6 | def init_templates(template_dir='templates', loader_class=recompile.Loader):
7 | """Initialize the templating subsystem.
8 |
9 | :param template_dir: the directory where templates are stored. This
10 | argument is optional. The default is to load templates
11 | from a *templates* subdirectory.
12 | :param loader_class: the ``utemplate.Loader`` class to use when loading
13 | templates. This argument is optional. The default is
14 | the ``recompile.Loader`` class, which automatically
15 | recompiles templates when they change.
16 | """
17 | global _loader
18 | _loader = loader_class(None, template_dir)
19 |
20 |
21 | def render_template(template, *args, **kwargs):
22 | """Render a template.
23 |
24 | :param template: The filename of the template to render, relative to the
25 | configured template directory.
26 | :param args: Positional arguments to be passed to the render engine.
27 | :param kwargs: Keyword arguments to be passed to the render engine.
28 |
29 | The return value is an iterator that returns sections of rendered template.
30 | """
31 | if _loader is None: # pragma: no cover
32 | init_templates()
33 | render = _loader.load(template)
34 | return render(*args, **kwargs)
35 |
--------------------------------------------------------------------------------
/control-components-from-web-server/microdot_utemplate.py:
--------------------------------------------------------------------------------
1 | from utemplate import recompile
2 |
3 | _loader = None
4 |
5 |
6 | def init_templates(template_dir='templates', loader_class=recompile.Loader):
7 | """Initialize the templating subsystem.
8 |
9 | :param template_dir: the directory where templates are stored. This
10 | argument is optional. The default is to load templates
11 | from a *templates* subdirectory.
12 | :param loader_class: the ``utemplate.Loader`` class to use when loading
13 | templates. This argument is optional. The default is
14 | the ``recompile.Loader`` class, which automatically
15 | recompiles templates when they change.
16 | """
17 | global _loader
18 | _loader = loader_class(None, template_dir)
19 |
20 |
21 | def render_template(template, *args, **kwargs):
22 | """Render a template.
23 |
24 | :param template: The filename of the template to render, relative to the
25 | configured template directory.
26 | :param args: Positional arguments to be passed to the render engine.
27 | :param kwargs: Keyword arguments to be passed to the render engine.
28 |
29 | The return value is an iterator that returns sections of rendered template.
30 | """
31 | if _loader is None: # pragma: no cover
32 | init_templates()
33 | render = _loader.load(template)
34 | return render(*args, **kwargs)
35 |
--------------------------------------------------------------------------------
/microdot-dynamic-component-path/microdot_utemplate.py:
--------------------------------------------------------------------------------
1 | from utemplate import recompile
2 |
3 | _loader = None
4 |
5 |
6 | def init_templates(template_dir='templates', loader_class=recompile.Loader):
7 | """Initialize the templating subsystem.
8 |
9 | :param template_dir: the directory where templates are stored. This
10 | argument is optional. The default is to load templates
11 | from a *templates* subdirectory.
12 | :param loader_class: the ``utemplate.Loader`` class to use when loading
13 | templates. This argument is optional. The default is
14 | the ``recompile.Loader`` class, which automatically
15 | recompiles templates when they change.
16 | """
17 | global _loader
18 | _loader = loader_class(None, template_dir)
19 |
20 |
21 | def render_template(template, *args, **kwargs):
22 | """Render a template.
23 |
24 | :param template: The filename of the template to render, relative to the
25 | configured template directory.
26 | :param args: Positional arguments to be passed to the render engine.
27 | :param kwargs: Keyword arguments to be passed to the render engine.
28 |
29 | The return value is an iterator that returns sections of rendered template.
30 | """
31 | if _loader is None: # pragma: no cover
32 | init_templates()
33 | render = _loader.load(template)
34 | return render(*args, **kwargs)
35 |
--------------------------------------------------------------------------------
/microdot-dynamic-component-path/rgb_led.py:
--------------------------------------------------------------------------------
1 | import time
2 | from machine import Pin,PWM
3 |
4 | #RGB
5 | RED = 0
6 | GREEN = 1
7 | BLUE = 2
8 |
9 | # Class that wil interface with our RGB Module
10 | class RGBLEDModule:
11 | def __init__(self, pwm_pins):
12 | self.pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])),
13 | PWM(Pin(pwm_pins[BLUE]))]
14 | self.init_pwms()
15 |
16 | # Initialize PWM Pins
17 | def init_pwms(self):
18 | for pwm in self.pwms:
19 | pwm.freq(1000)
20 |
21 | # Deinitialize PWM fins
22 | def deinit_pwms(self):
23 | self.turn_off_rgb()
24 | for pwm in self.pwms:
25 | pwm.deinit()
26 |
27 | # Map RGB values from 0-255 to duty cycle 0-65535
28 | def map_range(self, x, in_min, in_max, out_min, out_max):
29 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
30 |
31 | # Turn off RGB
32 | def turn_off_rgb(self):
33 | self.pwms[RED].duty_u16(0)
34 | self.pwms[GREEN].duty_u16(0)
35 | self.pwms[BLUE].duty_u16(0)
36 | time.sleep(0.1)
37 |
38 | # Set RGB Color
39 | def set_rgb_color(self, color):
40 | red, green, blue = color
41 |
42 | self.turn_off_rgb()
43 |
44 | self.pwms[RED].duty_u16(self.map_range(red, 0, 255, 0, 65535))
45 | self.pwms[GREEN].duty_u16(self.map_range(green, 0, 255, 0, 65535))
46 | self.pwms[BLUE].duty_u16(self.map_range(blue, 0, 255, 0, 65535))
47 |
--------------------------------------------------------------------------------
/microdot-dynamic-component-path/main.py:
--------------------------------------------------------------------------------
1 | from microdot_asyncio import Microdot, Response, send_file
2 | from microdot_utemplate import render_template
3 | from color_service import ReadColorsService
4 | from rgb_led import RGBLEDModule
5 |
6 | # Initialize MicroDot
7 | app = Microdot()
8 | Response.default_content_type = 'text/html'
9 |
10 | # Read the colors
11 | color_service = ReadColorsService()
12 | led_colors = color_service.read_colors()
13 |
14 | # Set the GPIO pins
15 | rgb_led_module = RGBLEDModule([13 , 14, 15])
16 |
17 | # root route
18 | @app.route('/')
19 | async def index(request):
20 | return render_template('index.html', colors=led_colors)
21 |
22 | # toggle RGB Module color
23 | @app.route('/toggle-led/')
24 | async def index(request, color):
25 | for led_color in led_colors:
26 | if color == led_color['name']:
27 | rgb_led_module.set_rgb_color(led_color['rgb'])
28 | break
29 | return {"status": "OK"}
30 |
31 | # Static CSS/JSS
32 | @app.route("/static/")
33 | def static(request, path):
34 | if ".." in path:
35 | # directory traversal is not allowed
36 | return "Not found", 404
37 | return send_file("static/" + path)
38 |
39 | # shutdown
40 | @app.get('/shutdown')
41 | def shutdown(request):
42 | rgb_led_module.deinit_pwms()
43 | request.app.shutdown()
44 | return 'The server is shutting down...'
45 |
46 |
47 | if __name__ == "__main__":
48 | try:
49 | app.run()
50 | except KeyboardInterrupt:
51 | rgb_led_module.deinit_pwms()
52 |
--------------------------------------------------------------------------------
/control-components-from-web-server/main.py:
--------------------------------------------------------------------------------
1 | from microdot_asyncio import Microdot, Response, send_file
2 | from microdot_utemplate import render_template
3 | from microdot_asyncio_websocket import with_websocket
4 | from rgb_led import RGBLEDModule
5 | import time
6 | import ujson
7 |
8 | # initialize RGB
9 | pwm_pins = [14, 15, 16]
10 | rgb_led = RGBLEDModule(pwm_pins)
11 | # Set default color to 50% each for RGB
12 | rgb_led.set_rgb_color({'blue': '50', 'red': '50', 'green': '50'})
13 |
14 | # Initialize MicroDot
15 | app = Microdot()
16 | Response.default_content_type = 'text/html'
17 |
18 | # root route
19 | @app.route('/')
20 | async def index(request):
21 | return render_template('index.html')
22 |
23 | # initialize websocket
24 | @app.route('/ws')
25 | @with_websocket
26 | async def read_sensor(request, ws):
27 | while True:
28 | data = await ws.receive()
29 | rgb_color = ujson.loads(data)
30 | rgb_led.set_rgb_color(rgb_color)
31 | await ws.send("OK")
32 |
33 | # Static CSS/JSS
34 | @app.route("/static/")
35 | def static(request, path):
36 | if ".." in path:
37 | # directory traversal is not allowed
38 | return "Not found", 404
39 | return send_file("static/" + path)
40 |
41 |
42 | # shutdown
43 | @app.get('/shutdown')
44 | def shutdown(request):
45 | request.app.shutdown()
46 | return 'The server is shutting down...'
47 |
48 |
49 | if __name__ == "__main__":
50 | try:
51 | app.run()
52 | except KeyboardInterrupt:
53 | rgb_led.deinit_pwms()
54 | pass
55 |
--------------------------------------------------------------------------------
/control-components-from-web-server/rgb_led.py:
--------------------------------------------------------------------------------
1 | import time
2 | from machine import Pin,PWM
3 |
4 | #RGB
5 | RED = 0
6 | GREEN = 1
7 | BLUE = 2
8 |
9 | RED_COLOR = "red"
10 | GREEN_COLOR = "green"
11 | BLUE_COLOR = "blue"
12 |
13 | # Class that wil interface with our RGB Module
14 | class RGBLEDModule:
15 | def __init__(self, pwm_pins):
16 | self.pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])),
17 | PWM(Pin(pwm_pins[BLUE]))]
18 | self.init_pwms()
19 |
20 | # Initialize PWM Pins
21 | def init_pwms(self):
22 | for pwm in self.pwms:
23 | pwm.freq(1000)
24 |
25 | # Deinitialize PWM fins
26 | def deinit_pwms(self):
27 | self.turn_off_rgb()
28 | for pwm in self.pwms:
29 | pwm.deinit()
30 |
31 | # Map RGB values from 0-100 to duty cycle 0-65535
32 | def map_range(self, x, in_min, in_max, out_min, out_max):
33 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
34 |
35 | # Turn off RGB
36 | def turn_off_rgb(self):
37 | self.pwms[RED].duty_u16(0)
38 | self.pwms[GREEN].duty_u16(0)
39 | self.pwms[BLUE].duty_u16(0)
40 | time.sleep(0.01)
41 |
42 | # Set RGB Color
43 | def set_rgb_color(self, rgb_color):
44 | self.turn_off_rgb()
45 |
46 |
47 | self.pwms[RED].duty_u16(self.map_range(int(rgb_color[RED_COLOR]), 0, 100, 0, 65535))
48 | self.pwms[GREEN].duty_u16(self.map_range(int(rgb_color[GREEN_COLOR]), 0, 100, 0, 65535))
49 | self.pwms[BLUE].duty_u16(self.map_range(int(rgb_color[BLUE_COLOR]), 0, 100, 0, 65535))
50 |
--------------------------------------------------------------------------------
/rgb-module/custom_color.py:
--------------------------------------------------------------------------------
1 | import time
2 | from machine import Pin,PWM
3 |
4 | #RGB
5 | RED = 0
6 | GREEN = 1
7 | BLUE = 2
8 |
9 | # Declare pins
10 | pwm_pins = [13,14,15]
11 | # Setup pins for PWM
12 | pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])),
13 | PWM(Pin(pwm_pins[BLUE]))]
14 | # Set pwm frequency
15 | [pwm.freq(1000) for pwm in pwms]
16 |
17 | # Colors that we are going to display
18 | colors = {"fuschia": (255, 0, 255), "yellow": (255,255,0), "aqua": (0, 255, 255)
19 | , "orange": (230, 138 , 0), "white": (255, 255 , 255)}
20 |
21 | def map_range(x, in_min, in_max, out_min, out_max):
22 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
23 |
24 | def turn_off_rgb():
25 | pwms[RED].duty_u16(0)
26 | pwms[GREEN].duty_u16(0)
27 | pwms[BLUE].duty_u16(0)
28 | time.sleep(0.1)
29 |
30 | # Deinitialize PWM on all pins
31 | def deinit_pwm_pins():
32 | pwms[RED].deinit()
33 | pwms[GREEN].deinit()
34 | pwms[BLUE].deinit()
35 |
36 | # main function
37 | def main():
38 | while True:
39 | for key, color in colors.items():
40 | # Turn off each RGB
41 | turn_off_rgb()
42 |
43 | print(f"Displaying Color:: {key}")
44 | red, green, blue = color
45 |
46 | pwms[RED].duty_u16(map_range(red, 0, 255, 0, 65535))
47 | pwms[GREEN].duty_u16(map_range(green, 0, 255, 0, 65535))
48 | pwms[BLUE].duty_u16(map_range(blue, 0, 255, 0, 65535))
49 | time.sleep(2)
50 |
51 |
52 | if __name__ == "__main__":
53 | try:
54 | main()
55 | except KeyboardInterrupt:
56 | deinit_pwm_pins()
57 |
58 |
--------------------------------------------------------------------------------
/umqtt.simple/main.py:
--------------------------------------------------------------------------------
1 | import time
2 | import ubinascii
3 | from umqtt.simple import MQTTClient
4 | import machine
5 | import random
6 |
7 | # Default MQTT_BROKER to connect to
8 | MQTT_BROKER = "192.168.100.22"
9 | CLIENT_ID = ubinascii.hexlify(machine.unique_id())
10 | SUBSCRIBE_TOPIC = b"led"
11 | PUBLISH_TOPIC = b"temperature"
12 |
13 | # Setup built in PICO LED as Output
14 | led = machine.Pin("LED",machine.Pin.OUT)
15 |
16 | # Publish MQTT messages after every set timeout
17 | last_publish = time.time()
18 | publish_interval = 5
19 |
20 | # Received messages from subscriptions will be delivered to this callback
21 | def sub_cb(topic, msg):
22 | print((topic, msg))
23 | if msg.decode() == "ON":
24 | led.value(1)
25 | else:
26 | led.value(0)
27 |
28 |
29 | def reset():
30 | print("Resetting...")
31 | time.sleep(5)
32 | machine.reset()
33 |
34 | # Generate dummy random temperature readings
35 | def get_temperature_reading():
36 | return random.randint(20, 50)
37 |
38 | def main():
39 | print(f"Begin connection with MQTT Broker :: {MQTT_BROKER}")
40 | mqttClient = MQTTClient(CLIENT_ID, MQTT_BROKER, keepalive=60)
41 | mqttClient.set_callback(sub_cb)
42 | mqttClient.connect()
43 | mqttClient.subscribe(SUBSCRIBE_TOPIC)
44 | print(f"Connected to MQTT Broker :: {MQTT_BROKER}, and waiting for callback function to be called!")
45 | while True:
46 | # Non-blocking wait for message
47 | mqttClient.check_msg()
48 | global last_publish
49 | if (time.time() - last_publish) >= publish_interval:
50 | random_temp = get_temperature_reading()
51 | mqttClient.publish(PUBLISH_TOPIC, str(random_temp).encode())
52 | last_publish = time.time()
53 | time.sleep(1)
54 |
55 |
56 | if __name__ == "__main__":
57 | while True:
58 | try:
59 | main()
60 | except OSError as e:
61 | print("Error: " + str(e))
62 | reset()
63 |
64 |
--------------------------------------------------------------------------------
/control-components-from-web-server/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Raspberry Pi Pico W - Web Server to control components
8 |
9 |
10 |
11 |
12 |
Raspberry Pi Pico W - Web Server to control components
13 |
14 |
15 |
16 |
24 |
25 |
26 | 50
27 |
28 |
29 |
30 |
31 |
39 |
40 |
41 | 50
42 |
43 |
44 |
45 |
46 |
54 |
55 |
56 | 50
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/dc-motor-drv8833/main.py:
--------------------------------------------------------------------------------
1 | from robot_car import RobotCar
2 | import utime
3 |
4 | # Pico W GPIO Pin
5 | LEFT_MOTOR_PIN_1 = 16
6 | LEFT_MOTOR_PIN_2 = 17
7 | RIGHT_MOTOR_PIN_1 = 18
8 | RIGHT_MOTOR_PIN_2 = 19
9 |
10 | motor_pins = [LEFT_MOTOR_PIN_1, LEFT_MOTOR_PIN_2, RIGHT_MOTOR_PIN_1, RIGHT_MOTOR_PIN_2]
11 |
12 | # Create an instance of our robot car
13 | robot_car = RobotCar(motor_pins, 20000)
14 |
15 | if __name__ == '__main__':
16 | try:
17 | # Test forward, reverse, stop, turn left and turn right
18 | print("*********Testing forward, reverse and loop*********")
19 | for i in range(2):
20 | print("Moving forward")
21 | robot_car.move_forward()
22 | utime.sleep(2)
23 | print("Moving backward")
24 | robot_car.move_backward()
25 | utime.sleep(2)
26 | print("stop")
27 | robot_car.stop()
28 | utime.sleep(2)
29 | print("turn left")
30 | robot_car.turn_left()
31 | utime.sleep(2)
32 | print("turn right")
33 | robot_car.turn_right()
34 | utime.sleep(2)
35 |
36 | print("*********Testing speed*********")
37 | for i in range(2):
38 | print("Moving at 100% speed")
39 | robot_car.change_speed(100);
40 | robot_car.move_forward()
41 | utime.sleep(2)
42 |
43 | print("Moving at 50% speed")
44 | robot_car.change_speed(50);
45 | robot_car.move_forward()
46 | utime.sleep(2)
47 |
48 | print("Moving at 20% of speed")
49 | robot_car.change_speed(20);
50 | robot_car.move_forward()
51 | utime.sleep(2)
52 |
53 | print("Moving at 0% of speed or the slowest")
54 | robot_car.change_speed(0);
55 | robot_car.move_forward()
56 | utime.sleep(2)
57 |
58 | robot_car.deinit()
59 |
60 | except KeyboardInterrupt:
61 | robot_car.deinit()
--------------------------------------------------------------------------------
/control-components-from-web-server/static/index.js:
--------------------------------------------------------------------------------
1 | // Slider
2 | var redSlider = document.querySelector("#redSlider");
3 | var redValue = document.querySelector("#redValue");
4 |
5 | var greenSlider = document.querySelector("#greenSlider");
6 | var greenValue = document.querySelector("#greenValue");
7 |
8 | var blueSlider = document.querySelector("#blueSlider");
9 | var blueValue = document.querySelector("#blueValue");
10 |
11 | redSlider.addEventListener("change", () => {
12 | redValue.textContent = redSlider.value;
13 | sendMessage(
14 | JSON.stringify({
15 | red: redSlider.value,
16 | green: greenSlider.value,
17 | blue: blueSlider.value,
18 | })
19 | );
20 | });
21 |
22 | greenSlider.addEventListener("change", () => {
23 | greenValue.textContent = greenSlider.value;
24 | sendMessage(
25 | JSON.stringify({
26 | red: redSlider.value,
27 | green: greenSlider.value,
28 | blue: blueSlider.value,
29 | })
30 | );
31 | });
32 |
33 | blueSlider.addEventListener("change", () => {
34 | blueValue.textContent = blueSlider.value;
35 | sendMessage(
36 | JSON.stringify({
37 | red: redSlider.value,
38 | green: greenSlider.value,
39 | blue: blueSlider.value,
40 | })
41 | );
42 | });
43 |
44 | // WebSocket support
45 | var targetUrl = `ws://${location.host}/ws`;
46 | var websocket;
47 | window.addEventListener("load", onLoad);
48 |
49 | function onLoad() {
50 | initializeSocket();
51 | }
52 |
53 | function initializeSocket() {
54 | console.log("Opening WebSocket connection MicroPython Server...");
55 | websocket = new WebSocket(targetUrl);
56 | websocket.onopen = onOpen;
57 | websocket.onclose = onClose;
58 | websocket.onmessage = onMessage;
59 | }
60 | function onOpen(event) {
61 | console.log("Starting connection to WebSocket server..");
62 | }
63 | function onClose(event) {
64 | console.log("Closing connection to server..");
65 | setTimeout(initializeSocket, 2000);
66 | }
67 | function onMessage(event) {
68 | console.log("WebSocket message received:", event);
69 | }
70 |
71 | function sendMessage(message) {
72 | websocket.send(message);
73 | }
74 |
--------------------------------------------------------------------------------
/mqtt-bme280-weather-station/main.py:
--------------------------------------------------------------------------------
1 | import time
2 | import ubinascii
3 | from umqtt.simple import MQTTClient
4 | import machine
5 | import random
6 | from bme_module import BME280Module
7 | import ujson
8 |
9 |
10 | # Default MQTT_BROKER to connect to
11 | MQTT_BROKER = "192.168.100.22"
12 | CLIENT_ID = ubinascii.hexlify(machine.unique_id())
13 | SUBSCRIBE_TOPIC = b"led"
14 | PUBLISH_TOPIC = b"sensorReadings"
15 |
16 | # Publish MQTT messages after every set timeout
17 | last_publish = time.time()
18 | publish_interval = 5
19 |
20 | # Pin assignment
21 | I2C_ID = 0
22 | SCL_PIN = 1
23 | SDA_PIN = 0
24 | bme_module = BME280Module(I2C_ID,SCL_PIN,SDA_PIN)
25 |
26 | # Received messages from subscriptions will be delivered to this callback
27 | def sub_cb(topic, msg):
28 | print((topic, msg))
29 | if msg.decode() == "ON":
30 | led.value(1)
31 | else:
32 | led.value(0)
33 |
34 | # Reset the device in case of error
35 | def reset():
36 | print("Resetting...")
37 | time.sleep(5)
38 | machine.reset()
39 |
40 | # Read the BMP/BME280 readings
41 | def get_temperature_reading():
42 | return bme_module.get_sensor_readings()
43 |
44 | # Main program
45 | def main():
46 | print(f"Begin connection with MQTT Broker :: {MQTT_BROKER}")
47 | mqttClient = MQTTClient(CLIENT_ID, MQTT_BROKER, keepalive=60)
48 | mqttClient.set_callback(sub_cb)
49 | mqttClient.connect()
50 | mqttClient.subscribe(SUBSCRIBE_TOPIC)
51 | print(f"Connected to MQTT Broker :: {MQTT_BROKER}, and waiting for callback function to be called!")
52 | while True:
53 | # Non-blocking wait for message
54 | mqttClient.check_msg()
55 | global last_publish
56 | current_time = time.time()
57 | if (current_time - last_publish) >= publish_interval:
58 | temperature, pressure, humidity, altitude = get_temperature_reading()
59 | readings = {"temperature": temperature, "pressure": pressure,"humidity": humidity, "altitude": altitude}
60 |
61 | mqttClient.publish(PUBLISH_TOPIC, ujson.dumps(readings).encode())
62 | last_publish = current_time
63 | time.sleep(1)
64 |
65 |
66 | if __name__ == "__main__":
67 | while True:
68 | try:
69 | main()
70 | except OSError as e:
71 | print("Error: " + str(e))
72 | reset()
73 |
--------------------------------------------------------------------------------
/websocket_using_microdot/static/index.js:
--------------------------------------------------------------------------------
1 | const sensorValues = document.querySelector("#sensor-values");
2 |
3 | const sensorData = [];
4 |
5 | /*
6 | Plotly.js graph and chart setup code
7 | */
8 | var sensorChartDiv = document.getElementById("sensor-chart");
9 |
10 | // History Data
11 | var sensorTrace = {
12 | x: [],
13 | y: [],
14 | name: "LDR/Photoresistor",
15 | mode: "lines+markers",
16 | type: "line",
17 | };
18 |
19 | var sensorLayout = {
20 | autosize: false,
21 | width: 800,
22 | height: 500,
23 | colorway: ["#05AD86"],
24 | margin: { t: 40, b: 40, l: 80, r: 80, pad: 0 },
25 | xaxis: {
26 | gridwidth: "2",
27 | autorange: true,
28 | },
29 | yaxis: {
30 | gridwidth: "2",
31 | autorange: true,
32 | },
33 | };
34 | var config = { responsive: true };
35 |
36 | Plotly.newPlot(sensorChartDiv, [sensorTrace], sensorLayout, config);
37 |
38 | // Will hold the sensor reads
39 | let newSensorXArray = [];
40 | let newSensorYArray = [];
41 |
42 | // The maximum number of data points displayed on our scatter/line graph
43 | let MAX_GRAPH_POINTS = 50;
44 | let ctr = 0;
45 |
46 | function updateChart(sensorRead) {
47 | if (newSensorXArray.length >= MAX_GRAPH_POINTS) {
48 | newSensorXArray.shift();
49 | }
50 | if (newSensorYArray.length >= MAX_GRAPH_POINTS) {
51 | newSensorYArray.shift();
52 | }
53 | newSensorXArray.push(ctr++);
54 | newSensorYArray.push(sensorRead);
55 |
56 | var data_update = {
57 | x: [newSensorXArray],
58 | y: [newSensorYArray],
59 | };
60 |
61 | Plotly.update(sensorChartDiv, data_update);
62 | }
63 |
64 | // WebSocket support
65 | var targetUrl = `ws://${location.host}/ws`;
66 | var websocket;
67 | window.addEventListener("load", onLoad);
68 |
69 | function onLoad() {
70 | initializeSocket();
71 | }
72 |
73 | function initializeSocket() {
74 | console.log("Opening WebSocket connection MicroPython Server...");
75 | websocket = new WebSocket(targetUrl);
76 | websocket.onopen = onOpen;
77 | websocket.onclose = onClose;
78 | websocket.onmessage = onMessage;
79 | }
80 | function onOpen(event) {
81 | console.log("Starting connection to WebSocket server..");
82 | }
83 | function onClose(event) {
84 | console.log("Closing connection to server..");
85 | setTimeout(initializeSocket, 2000);
86 | }
87 | function onMessage(event) {
88 | console.log("WebSocket message received:", event);
89 | updateValues(event.data);
90 | updateChart(event.data);
91 | }
92 |
93 | function sendMessage(message) {
94 | websocket.send(message);
95 | }
96 |
97 | function updateValues(data) {
98 | sensorData.unshift(data);
99 | if (sensorData.length > 20) sensorData.pop();
100 | sensorValues.value = sensorData.join("\r\n");
101 | }
102 |
--------------------------------------------------------------------------------
/dc-motor-drv8833/robot_car.py:
--------------------------------------------------------------------------------
1 |
2 | from machine import Pin
3 | from machine import PWM
4 | import utime
5 |
6 | '''
7 | Class to represent our robot car
8 | '''
9 | class RobotCar:
10 | MAX_DUTY_CYCLE = 65535
11 | MIN_DUTY_CYCLE = 0
12 | def __init__(self, motor_pins, frequency=20000):
13 | self.left_motor_pin1 = PWM(Pin(motor_pins[0], mode=Pin.OUT))
14 | self.left_motor_pin2 = PWM(Pin(motor_pins[1], mode=Pin.OUT))
15 | self.right_motor_pin1 = PWM(Pin(motor_pins[2], mode=Pin.OUT))
16 | self.right_motor_pin2 = PWM(Pin(motor_pins[3], mode=Pin.OUT))
17 | # set PWM frequency
18 | self.left_motor_pin1.freq(frequency)
19 | self.left_motor_pin2.freq(frequency)
20 | self.right_motor_pin1.freq(frequency)
21 | self.right_motor_pin2.freq(frequency)
22 |
23 | self.current_speed = RobotCar.MAX_DUTY_CYCLE
24 |
25 | def move_forward(self):
26 | self.left_motor_pin1.duty_u16(self.current_speed)
27 | self.left_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE)
28 |
29 | self.right_motor_pin1.duty_u16(self.current_speed)
30 | self.right_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE)
31 |
32 | def move_backward(self):
33 | self.left_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE)
34 | self.left_motor_pin2.duty_u16(self.current_speed)
35 |
36 | self.right_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE)
37 | self.right_motor_pin2.duty_u16(self.current_speed)
38 |
39 | def turn_left(self):
40 | self.left_motor_pin1.duty_u16(self.current_speed)
41 | self.left_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE)
42 |
43 | self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE)
44 | self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE)
45 |
46 | def turn_right(self):
47 | self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE)
48 | self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE)
49 |
50 | self.right_motor_pin1.duty_u16(self.current_speed)
51 | self.right_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE)
52 |
53 | def stop(self):
54 | self.left_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE)
55 | self.left_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE)
56 |
57 | self.right_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE)
58 | self.right_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE)
59 |
60 | ''' Map duty cycle values from 0-100 to duty cycle 40000-65535 '''
61 | def __map_range(self, x, in_min, in_max, out_min, out_max):
62 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
63 |
64 | ''' new_speed is a value from 0% - 100% '''
65 | def change_speed(self, new_speed):
66 | new_duty_cycle = self.__map_range(new_speed, 0, 100, 40000, 65535)
67 | self.current_speed = new_duty_cycle
68 |
69 |
70 | def deinit(self):
71 | """deinit PWM Pins"""
72 | print("Deinitializing PWM Pins")
73 | self.stop()
74 | utime.sleep(0.1)
75 | self.left_motor_pin1.deinit()
76 | self.left_motor_pin2.deinit()
77 | self.right_motor_pin1.deinit()
78 | self.right_motor_pin2.deinit()
79 |
80 |
--------------------------------------------------------------------------------
/websocket_using_microdot/microdot_asyncio_websocket.py:
--------------------------------------------------------------------------------
1 | from microdot_asyncio import Response
2 | from microdot_websocket import WebSocket as BaseWebSocket
3 |
4 |
5 | class WebSocket(BaseWebSocket):
6 | async def handshake(self):
7 | response = self._handshake_response()
8 | await self.request.sock[1].awrite(
9 | b'HTTP/1.1 101 Switching Protocols\r\n')
10 | await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
11 | await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
12 | await self.request.sock[1].awrite(
13 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
14 |
15 | async def receive(self):
16 | while True:
17 | opcode, payload = await self._read_frame()
18 | send_opcode, data = self._process_websocket_frame(opcode, payload)
19 | if send_opcode: # pragma: no cover
20 | await self.send(send_opcode, data)
21 | elif data: # pragma: no branch
22 | return data
23 |
24 | async def send(self, data, opcode=None):
25 | frame = self._encode_websocket_frame(
26 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
27 | data)
28 | await self.request.sock[1].awrite(frame)
29 |
30 | async def close(self):
31 | if not self.closed: # pragma: no cover
32 | self.closed = True
33 | await self.send(b'', self.CLOSE)
34 |
35 | async def _read_frame(self):
36 | header = await self.request.sock[0].read(2)
37 | if len(header) != 2: # pragma: no cover
38 | raise OSError(32, 'Websocket connection closed')
39 | fin, opcode, has_mask, length = self._parse_frame_header(header)
40 | if length == -2:
41 | length = await self.request.sock[0].read(2)
42 | length = int.from_bytes(length, 'big')
43 | elif length == -8:
44 | length = await self.request.sock[0].read(8)
45 | length = int.from_bytes(length, 'big')
46 | if has_mask: # pragma: no cover
47 | mask = await self.request.sock[0].read(4)
48 | payload = await self.request.sock[0].read(length)
49 | if has_mask: # pragma: no cover
50 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
51 | return opcode, payload
52 |
53 |
54 | async def websocket_upgrade(request):
55 | """Upgrade a request handler to a websocket connection.
56 |
57 | This function can be called directly inside a route function to process a
58 | WebSocket upgrade handshake, for example after the user's credentials are
59 | verified. The function returns the websocket object::
60 |
61 | @app.route('/echo')
62 | async def echo(request):
63 | if not authenticate_user(request):
64 | abort(401)
65 | ws = await websocket_upgrade(request)
66 | while True:
67 | message = await ws.receive()
68 | await ws.send(message)
69 | """
70 | ws = WebSocket(request)
71 | await ws.handshake()
72 |
73 | @request.after_request
74 | async def after_request(request, response):
75 | return Response.already_handled
76 |
77 | return ws
78 |
79 |
80 | def with_websocket(f):
81 | """Decorator to make a route a WebSocket endpoint.
82 |
83 | This decorator is used to define a route that accepts websocket
84 | connections. The route then receives a websocket object as a second
85 | argument that it can use to send and receive messages::
86 |
87 | @app.route('/echo')
88 | @with_websocket
89 | async def echo(request, ws):
90 | while True:
91 | message = await ws.receive()
92 | await ws.send(message)
93 | """
94 | async def wrapper(request, *args, **kwargs):
95 | ws = await websocket_upgrade(request)
96 | try:
97 | await f(request, ws, *args, **kwargs)
98 | await ws.close() # pragma: no cover
99 | except OSError as exc:
100 | if exc.errno not in [32, 54, 104]: # pragma: no cover
101 | raise
102 | return ''
103 | return wrapper
104 |
--------------------------------------------------------------------------------
/control-components-from-web-server/microdot_asyncio_websocket.py:
--------------------------------------------------------------------------------
1 | from microdot_asyncio import Response
2 | from microdot_websocket import WebSocket as BaseWebSocket
3 |
4 |
5 | class WebSocket(BaseWebSocket):
6 | async def handshake(self):
7 | response = self._handshake_response()
8 | await self.request.sock[1].awrite(
9 | b'HTTP/1.1 101 Switching Protocols\r\n')
10 | await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
11 | await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
12 | await self.request.sock[1].awrite(
13 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
14 |
15 | async def receive(self):
16 | while True:
17 | opcode, payload = await self._read_frame()
18 | send_opcode, data = self._process_websocket_frame(opcode, payload)
19 | if send_opcode: # pragma: no cover
20 | await self.send(send_opcode, data)
21 | elif data: # pragma: no branch
22 | return data
23 |
24 | async def send(self, data, opcode=None):
25 | frame = self._encode_websocket_frame(
26 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
27 | data)
28 | await self.request.sock[1].awrite(frame)
29 |
30 | async def close(self):
31 | if not self.closed: # pragma: no cover
32 | self.closed = True
33 | await self.send(b'', self.CLOSE)
34 |
35 | async def _read_frame(self):
36 | header = await self.request.sock[0].read(2)
37 | if len(header) != 2: # pragma: no cover
38 | raise OSError(32, 'Websocket connection closed')
39 | fin, opcode, has_mask, length = self._parse_frame_header(header)
40 | if length == -2:
41 | length = await self.request.sock[0].read(2)
42 | length = int.from_bytes(length, 'big')
43 | elif length == -8:
44 | length = await self.request.sock[0].read(8)
45 | length = int.from_bytes(length, 'big')
46 | if has_mask: # pragma: no cover
47 | mask = await self.request.sock[0].read(4)
48 | payload = await self.request.sock[0].read(length)
49 | if has_mask: # pragma: no cover
50 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
51 | return opcode, payload
52 |
53 |
54 | async def websocket_upgrade(request):
55 | """Upgrade a request handler to a websocket connection.
56 |
57 | This function can be called directly inside a route function to process a
58 | WebSocket upgrade handshake, for example after the user's credentials are
59 | verified. The function returns the websocket object::
60 |
61 | @app.route('/echo')
62 | async def echo(request):
63 | if not authenticate_user(request):
64 | abort(401)
65 | ws = await websocket_upgrade(request)
66 | while True:
67 | message = await ws.receive()
68 | await ws.send(message)
69 | """
70 | ws = WebSocket(request)
71 | await ws.handshake()
72 |
73 | @request.after_request
74 | async def after_request(request, response):
75 | return Response.already_handled
76 |
77 | return ws
78 |
79 |
80 | def with_websocket(f):
81 | """Decorator to make a route a WebSocket endpoint.
82 |
83 | This decorator is used to define a route that accepts websocket
84 | connections. The route then receives a websocket object as a second
85 | argument that it can use to send and receive messages::
86 |
87 | @app.route('/echo')
88 | @with_websocket
89 | async def echo(request, ws):
90 | while True:
91 | message = await ws.receive()
92 | await ws.send(message)
93 | """
94 | async def wrapper(request, *args, **kwargs):
95 | ws = await websocket_upgrade(request)
96 | try:
97 | await f(request, ws, *args, **kwargs)
98 | await ws.close() # pragma: no cover
99 | except OSError as exc:
100 | if exc.errno not in [32, 54, 104]: # pragma: no cover
101 | raise
102 | return ''
103 | return wrapper
104 |
--------------------------------------------------------------------------------
/websocket_using_microdot/microdot_websocket.py:
--------------------------------------------------------------------------------
1 | import binascii
2 | import hashlib
3 | from microdot import Response
4 |
5 |
6 | class WebSocket:
7 | CONT = 0
8 | TEXT = 1
9 | BINARY = 2
10 | CLOSE = 8
11 | PING = 9
12 | PONG = 10
13 |
14 | def __init__(self, request):
15 | self.request = request
16 | self.closed = False
17 |
18 | def handshake(self):
19 | response = self._handshake_response()
20 | self.request.sock.send(b'HTTP/1.1 101 Switching Protocols\r\n')
21 | self.request.sock.send(b'Upgrade: websocket\r\n')
22 | self.request.sock.send(b'Connection: Upgrade\r\n')
23 | self.request.sock.send(
24 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
25 |
26 | def receive(self):
27 | while True:
28 | opcode, payload = self._read_frame()
29 | send_opcode, data = self._process_websocket_frame(opcode, payload)
30 | if send_opcode: # pragma: no cover
31 | self.send(send_opcode, data)
32 | elif data: # pragma: no branch
33 | return data
34 |
35 | def send(self, data, opcode=None):
36 | frame = self._encode_websocket_frame(
37 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
38 | data)
39 | self.request.sock.send(frame)
40 |
41 | def close(self):
42 | if not self.closed: # pragma: no cover
43 | self.closed = True
44 | self.send(b'', self.CLOSE)
45 |
46 | def _handshake_response(self):
47 | connection = False
48 | upgrade = False
49 | websocket_key = None
50 | for header, value in self.request.headers.items():
51 | h = header.lower()
52 | if h == 'connection':
53 | connection = True
54 | if 'upgrade' not in value.lower():
55 | return self.request.app.abort(400)
56 | elif h == 'upgrade':
57 | upgrade = True
58 | if not value.lower() == 'websocket':
59 | return self.request.app.abort(400)
60 | elif h == 'sec-websocket-key':
61 | websocket_key = value
62 | if not connection or not upgrade or not websocket_key:
63 | return self.request.app.abort(400)
64 | d = hashlib.sha1(websocket_key.encode())
65 | d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
66 | return binascii.b2a_base64(d.digest())[:-1]
67 |
68 | @classmethod
69 | def _parse_frame_header(cls, header):
70 | fin = header[0] & 0x80
71 | opcode = header[0] & 0x0f
72 | if fin == 0 or opcode == cls.CONT: # pragma: no cover
73 | raise OSError(32, 'Continuation frames not supported')
74 | has_mask = header[1] & 0x80
75 | length = header[1] & 0x7f
76 | if length == 126:
77 | length = -2
78 | elif length == 127:
79 | length = -8
80 | return fin, opcode, has_mask, length
81 |
82 | def _process_websocket_frame(self, opcode, payload):
83 | if opcode == self.TEXT:
84 | payload = payload.decode()
85 | elif opcode == self.BINARY:
86 | pass
87 | elif opcode == self.CLOSE:
88 | raise OSError(32, 'Websocket connection closed')
89 | elif opcode == self.PING:
90 | return self.PONG, payload
91 | elif opcode == self.PONG: # pragma: no branch
92 | return None, None
93 | return None, payload
94 |
95 | @classmethod
96 | def _encode_websocket_frame(cls, opcode, payload):
97 | frame = bytearray()
98 | frame.append(0x80 | opcode)
99 | if opcode == cls.TEXT:
100 | payload = payload.encode()
101 | if len(payload) < 126:
102 | frame.append(len(payload))
103 | elif len(payload) < (1 << 16):
104 | frame.append(126)
105 | frame.extend(len(payload).to_bytes(2, 'big'))
106 | else:
107 | frame.append(127)
108 | frame.extend(len(payload).to_bytes(8, 'big'))
109 | frame.extend(payload)
110 | return frame
111 |
112 | def _read_frame(self):
113 | header = self.request.sock.recv(2)
114 | if len(header) != 2: # pragma: no cover
115 | raise OSError(32, 'Websocket connection closed')
116 | fin, opcode, has_mask, length = self._parse_frame_header(header)
117 | if length < 0:
118 | length = self.request.sock.recv(-length)
119 | length = int.from_bytes(length, 'big')
120 | if has_mask: # pragma: no cover
121 | mask = self.request.sock.recv(4)
122 | payload = self.request.sock.recv(length)
123 | if has_mask: # pragma: no cover
124 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
125 | return opcode, payload
126 |
127 |
128 | def websocket_upgrade(request):
129 | """Upgrade a request handler to a websocket connection.
130 |
131 | This function can be called directly inside a route function to process a
132 | WebSocket upgrade handshake, for example after the user's credentials are
133 | verified. The function returns the websocket object::
134 |
135 | @app.route('/echo')
136 | def echo(request):
137 | if not authenticate_user(request):
138 | abort(401)
139 | ws = websocket_upgrade(request)
140 | while True:
141 | message = ws.receive()
142 | ws.send(message)
143 | """
144 | ws = WebSocket(request)
145 | ws.handshake()
146 |
147 | @request.after_request
148 | def after_request(request, response):
149 | return Response.already_handled
150 |
151 | return ws
152 |
153 |
154 | def with_websocket(f):
155 | """Decorator to make a route a WebSocket endpoint.
156 |
157 | This decorator is used to define a route that accepts websocket
158 | connections. The route then receives a websocket object as a second
159 | argument that it can use to send and receive messages::
160 |
161 | @app.route('/echo')
162 | @with_websocket
163 | def echo(request, ws):
164 | while True:
165 | message = ws.receive()
166 | ws.send(message)
167 | """
168 | def wrapper(request, *args, **kwargs):
169 | ws = websocket_upgrade(request)
170 | try:
171 | f(request, ws, *args, **kwargs)
172 | ws.close() # pragma: no cover
173 | except OSError as exc:
174 | if exc.errno not in [32, 54, 104]: # pragma: no cover
175 | raise
176 | return ''
177 | return wrapper
178 |
--------------------------------------------------------------------------------
/control-components-from-web-server/microdot_websocket.py:
--------------------------------------------------------------------------------
1 | import binascii
2 | import hashlib
3 | from microdot import Response
4 |
5 |
6 | class WebSocket:
7 | CONT = 0
8 | TEXT = 1
9 | BINARY = 2
10 | CLOSE = 8
11 | PING = 9
12 | PONG = 10
13 |
14 | def __init__(self, request):
15 | self.request = request
16 | self.closed = False
17 |
18 | def handshake(self):
19 | response = self._handshake_response()
20 | self.request.sock.send(b'HTTP/1.1 101 Switching Protocols\r\n')
21 | self.request.sock.send(b'Upgrade: websocket\r\n')
22 | self.request.sock.send(b'Connection: Upgrade\r\n')
23 | self.request.sock.send(
24 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
25 |
26 | def receive(self):
27 | while True:
28 | opcode, payload = self._read_frame()
29 | send_opcode, data = self._process_websocket_frame(opcode, payload)
30 | if send_opcode: # pragma: no cover
31 | self.send(send_opcode, data)
32 | elif data: # pragma: no branch
33 | return data
34 |
35 | def send(self, data, opcode=None):
36 | frame = self._encode_websocket_frame(
37 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
38 | data)
39 | self.request.sock.send(frame)
40 |
41 | def close(self):
42 | if not self.closed: # pragma: no cover
43 | self.closed = True
44 | self.send(b'', self.CLOSE)
45 |
46 | def _handshake_response(self):
47 | connection = False
48 | upgrade = False
49 | websocket_key = None
50 | for header, value in self.request.headers.items():
51 | h = header.lower()
52 | if h == 'connection':
53 | connection = True
54 | if 'upgrade' not in value.lower():
55 | return self.request.app.abort(400)
56 | elif h == 'upgrade':
57 | upgrade = True
58 | if not value.lower() == 'websocket':
59 | return self.request.app.abort(400)
60 | elif h == 'sec-websocket-key':
61 | websocket_key = value
62 | if not connection or not upgrade or not websocket_key:
63 | return self.request.app.abort(400)
64 | d = hashlib.sha1(websocket_key.encode())
65 | d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
66 | return binascii.b2a_base64(d.digest())[:-1]
67 |
68 | @classmethod
69 | def _parse_frame_header(cls, header):
70 | fin = header[0] & 0x80
71 | opcode = header[0] & 0x0f
72 | if fin == 0 or opcode == cls.CONT: # pragma: no cover
73 | raise OSError(32, 'Continuation frames not supported')
74 | has_mask = header[1] & 0x80
75 | length = header[1] & 0x7f
76 | if length == 126:
77 | length = -2
78 | elif length == 127:
79 | length = -8
80 | return fin, opcode, has_mask, length
81 |
82 | def _process_websocket_frame(self, opcode, payload):
83 | if opcode == self.TEXT:
84 | payload = payload.decode()
85 | elif opcode == self.BINARY:
86 | pass
87 | elif opcode == self.CLOSE:
88 | raise OSError(32, 'Websocket connection closed')
89 | elif opcode == self.PING:
90 | return self.PONG, payload
91 | elif opcode == self.PONG: # pragma: no branch
92 | return None, None
93 | return None, payload
94 |
95 | @classmethod
96 | def _encode_websocket_frame(cls, opcode, payload):
97 | frame = bytearray()
98 | frame.append(0x80 | opcode)
99 | if opcode == cls.TEXT:
100 | payload = payload.encode()
101 | if len(payload) < 126:
102 | frame.append(len(payload))
103 | elif len(payload) < (1 << 16):
104 | frame.append(126)
105 | frame.extend(len(payload).to_bytes(2, 'big'))
106 | else:
107 | frame.append(127)
108 | frame.extend(len(payload).to_bytes(8, 'big'))
109 | frame.extend(payload)
110 | return frame
111 |
112 | def _read_frame(self):
113 | header = self.request.sock.recv(2)
114 | if len(header) != 2: # pragma: no cover
115 | raise OSError(32, 'Websocket connection closed')
116 | fin, opcode, has_mask, length = self._parse_frame_header(header)
117 | if length < 0:
118 | length = self.request.sock.recv(-length)
119 | length = int.from_bytes(length, 'big')
120 | if has_mask: # pragma: no cover
121 | mask = self.request.sock.recv(4)
122 | payload = self.request.sock.recv(length)
123 | if has_mask: # pragma: no cover
124 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
125 | return opcode, payload
126 |
127 |
128 | def websocket_upgrade(request):
129 | """Upgrade a request handler to a websocket connection.
130 |
131 | This function can be called directly inside a route function to process a
132 | WebSocket upgrade handshake, for example after the user's credentials are
133 | verified. The function returns the websocket object::
134 |
135 | @app.route('/echo')
136 | def echo(request):
137 | if not authenticate_user(request):
138 | abort(401)
139 | ws = websocket_upgrade(request)
140 | while True:
141 | message = ws.receive()
142 | ws.send(message)
143 | """
144 | ws = WebSocket(request)
145 | ws.handshake()
146 |
147 | @request.after_request
148 | def after_request(request, response):
149 | return Response.already_handled
150 |
151 | return ws
152 |
153 |
154 | def with_websocket(f):
155 | """Decorator to make a route a WebSocket endpoint.
156 |
157 | This decorator is used to define a route that accepts websocket
158 | connections. The route then receives a websocket object as a second
159 | argument that it can use to send and receive messages::
160 |
161 | @app.route('/echo')
162 | @with_websocket
163 | def echo(request, ws):
164 | while True:
165 | message = ws.receive()
166 | ws.send(message)
167 | """
168 | def wrapper(request, *args, **kwargs):
169 | ws = websocket_upgrade(request)
170 | try:
171 | f(request, ws, *args, **kwargs)
172 | ws.close() # pragma: no cover
173 | except OSError as exc:
174 | if exc.errno not in [32, 54, 104]: # pragma: no cover
175 | raise
176 | return ''
177 | return wrapper
178 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/microdot-dynamic-component-path/microdot_asyncio.py:
--------------------------------------------------------------------------------
1 | """
2 | microdot_asyncio
3 | ----------------
4 |
5 | The ``microdot_asyncio`` module defines a few classes that help implement
6 | HTTP-based servers for MicroPython and standard Python that use ``asyncio``
7 | and coroutines.
8 | """
9 | try:
10 | import uasyncio as asyncio
11 | except ImportError:
12 | import asyncio
13 |
14 | try:
15 | import uio as io
16 | except ImportError:
17 | import io
18 |
19 | from microdot import Microdot as BaseMicrodot
20 | from microdot import mro
21 | from microdot import NoCaseDict
22 | from microdot import Request as BaseRequest
23 | from microdot import Response as BaseResponse
24 | from microdot import print_exception
25 | from microdot import HTTPException
26 | from microdot import MUTED_SOCKET_ERRORS
27 |
28 |
29 | def _iscoroutine(coro):
30 | return hasattr(coro, 'send') and hasattr(coro, 'throw')
31 |
32 |
33 | class _AsyncBytesIO:
34 | def __init__(self, data):
35 | self.stream = io.BytesIO(data)
36 |
37 | async def read(self, n=-1):
38 | return self.stream.read(n)
39 |
40 | async def readline(self): # pragma: no cover
41 | return self.stream.readline()
42 |
43 | async def readexactly(self, n): # pragma: no cover
44 | return self.stream.read(n)
45 |
46 | async def readuntil(self, separator=b'\n'): # pragma: no cover
47 | return self.stream.readuntil(separator=separator)
48 |
49 | async def awrite(self, data): # pragma: no cover
50 | return self.stream.write(data)
51 |
52 | async def aclose(self): # pragma: no cover
53 | pass
54 |
55 |
56 | class Request(BaseRequest):
57 | @staticmethod
58 | async def create(app, client_reader, client_writer, client_addr):
59 | """Create a request object.
60 |
61 | :param app: The Microdot application instance.
62 | :param client_reader: An input stream from where the request data can
63 | be read.
64 | :param client_writer: An output stream where the response data can be
65 | written.
66 | :param client_addr: The address of the client, as a tuple.
67 |
68 | This method is a coroutine. It returns a newly created ``Request``
69 | object.
70 | """
71 | # request line
72 | line = (await Request._safe_readline(client_reader)).strip().decode()
73 | if not line:
74 | return None
75 | method, url, http_version = line.split()
76 | http_version = http_version.split('/', 1)[1]
77 |
78 | # headers
79 | headers = NoCaseDict()
80 | content_length = 0
81 | while True:
82 | line = (await Request._safe_readline(
83 | client_reader)).strip().decode()
84 | if line == '':
85 | break
86 | header, value = line.split(':', 1)
87 | value = value.strip()
88 | headers[header] = value
89 | if header.lower() == 'content-length':
90 | content_length = int(value)
91 |
92 | # body
93 | body = b''
94 | if content_length and content_length <= Request.max_body_length:
95 | body = await client_reader.readexactly(content_length)
96 | stream = None
97 | else:
98 | body = b''
99 | stream = client_reader
100 |
101 | return Request(app, client_addr, method, url, http_version, headers,
102 | body=body, stream=stream,
103 | sock=(client_reader, client_writer))
104 |
105 | @property
106 | def stream(self):
107 | if self._stream is None:
108 | self._stream = _AsyncBytesIO(self._body)
109 | return self._stream
110 |
111 | @staticmethod
112 | async def _safe_readline(stream):
113 | line = (await stream.readline())
114 | if len(line) > Request.max_readline:
115 | raise ValueError('line too long')
116 | return line
117 |
118 |
119 | class Response(BaseResponse):
120 | """An HTTP response class.
121 |
122 | :param body: The body of the response. If a dictionary or list is given,
123 | a JSON formatter is used to generate the body. If a file-like
124 | object or an async generator is given, a streaming response is
125 | used. If a string is given, it is encoded from UTF-8. Else,
126 | the body should be a byte sequence.
127 | :param status_code: The numeric HTTP status code of the response. The
128 | default is 200.
129 | :param headers: A dictionary of headers to include in the response.
130 | :param reason: A custom reason phrase to add after the status code. The
131 | default is "OK" for responses with a 200 status code and
132 | "N/A" for any other status codes.
133 | """
134 |
135 | async def write(self, stream):
136 | self.complete()
137 |
138 | try:
139 | # status code
140 | reason = self.reason if self.reason is not None else \
141 | ('OK' if self.status_code == 200 else 'N/A')
142 | await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
143 | status_code=self.status_code, reason=reason).encode())
144 |
145 | # headers
146 | for header, value in self.headers.items():
147 | values = value if isinstance(value, list) else [value]
148 | for value in values:
149 | await stream.awrite('{header}: {value}\r\n'.format(
150 | header=header, value=value).encode())
151 | await stream.awrite(b'\r\n')
152 |
153 | # body
154 | async for body in self.body_iter():
155 | if isinstance(body, str): # pragma: no cover
156 | body = body.encode()
157 | await stream.awrite(body)
158 | except OSError as exc: # pragma: no cover
159 | if exc.errno in MUTED_SOCKET_ERRORS or \
160 | exc.args[0] == 'Connection lost':
161 | pass
162 | else:
163 | raise
164 |
165 | def body_iter(self):
166 | if hasattr(self.body, '__anext__'):
167 | # response body is an async generator
168 | return self.body
169 |
170 | response = self
171 |
172 | class iter:
173 | def __aiter__(self):
174 | if response.body:
175 | self.i = 0 # need to determine type of response.body
176 | else:
177 | self.i = -1 # no response body
178 | return self
179 |
180 | async def __anext__(self):
181 | if self.i == -1:
182 | raise StopAsyncIteration
183 | if self.i == 0:
184 | if hasattr(response.body, 'read'):
185 | self.i = 2 # response body is a file-like object
186 | elif hasattr(response.body, '__next__'):
187 | self.i = 1 # response body is a sync generator
188 | return next(response.body)
189 | else:
190 | self.i = -1 # response body is a plain string
191 | return response.body
192 | elif self.i == 1:
193 | try:
194 | return next(response.body)
195 | except StopIteration:
196 | raise StopAsyncIteration
197 | buf = response.body.read(response.send_file_buffer_size)
198 | if _iscoroutine(buf): # pragma: no cover
199 | buf = await buf
200 | if len(buf) < response.send_file_buffer_size:
201 | self.i = -1
202 | if hasattr(response.body, 'close'): # pragma: no cover
203 | result = response.body.close()
204 | if _iscoroutine(result):
205 | await result
206 | return buf
207 |
208 | return iter()
209 |
210 |
211 | class Microdot(BaseMicrodot):
212 | async def start_server(self, host='0.0.0.0', port=5000, debug=False,
213 | ssl=None):
214 | """Start the Microdot web server as a coroutine. This coroutine does
215 | not normally return, as the server enters an endless listening loop.
216 | The :func:`shutdown` function provides a method for terminating the
217 | server gracefully.
218 |
219 | :param host: The hostname or IP address of the network interface that
220 | will be listening for requests. A value of ``'0.0.0.0'``
221 | (the default) indicates that the server should listen for
222 | requests on all the available interfaces, and a value of
223 | ``127.0.0.1`` indicates that the server should listen
224 | for requests only on the internal networking interface of
225 | the host.
226 | :param port: The port number to listen for requests. The default is
227 | port 5000.
228 | :param debug: If ``True``, the server logs debugging information. The
229 | default is ``False``.
230 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
231 | not use TLS. The default is ``None``.
232 |
233 | This method is a coroutine.
234 |
235 | Example::
236 |
237 | import asyncio
238 | from microdot_asyncio import Microdot
239 |
240 | app = Microdot()
241 |
242 | @app.route('/')
243 | async def index():
244 | return 'Hello, world!'
245 |
246 | async def main():
247 | await app.start_server(debug=True)
248 |
249 | asyncio.run(main())
250 | """
251 | self.debug = debug
252 |
253 | async def serve(reader, writer):
254 | if not hasattr(writer, 'awrite'): # pragma: no cover
255 | # CPython provides the awrite and aclose methods in 3.8+
256 | async def awrite(self, data):
257 | self.write(data)
258 | await self.drain()
259 |
260 | async def aclose(self):
261 | self.close()
262 | await self.wait_closed()
263 |
264 | from types import MethodType
265 | writer.awrite = MethodType(awrite, writer)
266 | writer.aclose = MethodType(aclose, writer)
267 |
268 | await self.handle_request(reader, writer)
269 |
270 | if self.debug: # pragma: no cover
271 | print('Starting async server on {host}:{port}...'.format(
272 | host=host, port=port))
273 |
274 | try:
275 | self.server = await asyncio.start_server(serve, host, port,
276 | ssl=ssl)
277 | except TypeError:
278 | self.server = await asyncio.start_server(serve, host, port)
279 |
280 | while True:
281 | try:
282 | await self.server.wait_closed()
283 | break
284 | except AttributeError: # pragma: no cover
285 | # the task hasn't been initialized in the server object yet
286 | # wait a bit and try again
287 | await asyncio.sleep(0.1)
288 |
289 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
290 | """Start the web server. This function does not normally return, as
291 | the server enters an endless listening loop. The :func:`shutdown`
292 | function provides a method for terminating the server gracefully.
293 |
294 | :param host: The hostname or IP address of the network interface that
295 | will be listening for requests. A value of ``'0.0.0.0'``
296 | (the default) indicates that the server should listen for
297 | requests on all the available interfaces, and a value of
298 | ``127.0.0.1`` indicates that the server should listen
299 | for requests only on the internal networking interface of
300 | the host.
301 | :param port: The port number to listen for requests. The default is
302 | port 5000.
303 | :param debug: If ``True``, the server logs debugging information. The
304 | default is ``False``.
305 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
306 | not use TLS. The default is ``None``.
307 |
308 | Example::
309 |
310 | from microdot_asyncio import Microdot
311 |
312 | app = Microdot()
313 |
314 | @app.route('/')
315 | async def index():
316 | return 'Hello, world!'
317 |
318 | app.run(debug=True)
319 | """
320 | asyncio.run(self.start_server(host=host, port=port, debug=debug,
321 | ssl=ssl))
322 |
323 | def shutdown(self):
324 | self.server.close()
325 |
326 | async def handle_request(self, reader, writer):
327 | req = None
328 | try:
329 | req = await Request.create(self, reader, writer,
330 | writer.get_extra_info('peername'))
331 | except Exception as exc: # pragma: no cover
332 | print_exception(exc)
333 |
334 | res = await self.dispatch_request(req)
335 | if res != Response.already_handled: # pragma: no branch
336 | await res.write(writer)
337 | try:
338 | await writer.aclose()
339 | except OSError as exc: # pragma: no cover
340 | if exc.errno in MUTED_SOCKET_ERRORS:
341 | pass
342 | else:
343 | raise
344 | if self.debug and req: # pragma: no cover
345 | print('{method} {path} {status_code}'.format(
346 | method=req.method, path=req.path,
347 | status_code=res.status_code))
348 |
349 | async def dispatch_request(self, req):
350 | if req:
351 | if req.content_length > req.max_content_length:
352 | if 413 in self.error_handlers:
353 | res = await self._invoke_handler(
354 | self.error_handlers[413], req)
355 | else:
356 | res = 'Payload too large', 413
357 | else:
358 | f = self.find_route(req)
359 | try:
360 | res = None
361 | if callable(f):
362 | for handler in self.before_request_handlers:
363 | res = await self._invoke_handler(handler, req)
364 | if res:
365 | break
366 | if res is None:
367 | res = await self._invoke_handler(
368 | f, req, **req.url_args)
369 | if isinstance(res, tuple):
370 | body = res[0]
371 | if isinstance(res[1], int):
372 | status_code = res[1]
373 | headers = res[2] if len(res) > 2 else {}
374 | else:
375 | status_code = 200
376 | headers = res[1]
377 | res = Response(body, status_code, headers)
378 | elif not isinstance(res, Response):
379 | res = Response(res)
380 | for handler in self.after_request_handlers:
381 | res = await self._invoke_handler(
382 | handler, req, res) or res
383 | for handler in req.after_request_handlers:
384 | res = await self._invoke_handler(
385 | handler, req, res) or res
386 | elif f in self.error_handlers:
387 | res = await self._invoke_handler(
388 | self.error_handlers[f], req)
389 | else:
390 | res = 'Not found', f
391 | except HTTPException as exc:
392 | if exc.status_code in self.error_handlers:
393 | res = self.error_handlers[exc.status_code](req)
394 | else:
395 | res = exc.reason, exc.status_code
396 | except Exception as exc:
397 | print_exception(exc)
398 | exc_class = None
399 | res = None
400 | if exc.__class__ in self.error_handlers:
401 | exc_class = exc.__class__
402 | else:
403 | for c in mro(exc.__class__)[1:]:
404 | if c in self.error_handlers:
405 | exc_class = c
406 | break
407 | if exc_class:
408 | try:
409 | res = await self._invoke_handler(
410 | self.error_handlers[exc_class], req, exc)
411 | except Exception as exc2: # pragma: no cover
412 | print_exception(exc2)
413 | if res is None:
414 | if 500 in self.error_handlers:
415 | res = await self._invoke_handler(
416 | self.error_handlers[500], req)
417 | else:
418 | res = 'Internal server error', 500
419 | else:
420 | if 400 in self.error_handlers:
421 | res = await self._invoke_handler(self.error_handlers[400], req)
422 | else:
423 | res = 'Bad request', 400
424 | if isinstance(res, tuple):
425 | res = Response(*res)
426 | elif not isinstance(res, Response):
427 | res = Response(res)
428 | return res
429 |
430 | async def _invoke_handler(self, f_or_coro, *args, **kwargs):
431 | ret = f_or_coro(*args, **kwargs)
432 | if _iscoroutine(ret):
433 | ret = await ret
434 | return ret
435 |
436 |
437 | abort = Microdot.abort
438 | Response.already_handled = Response()
439 | redirect = Response.redirect
440 | send_file = Response.send_file
441 |
--------------------------------------------------------------------------------
/websocket_using_microdot/microdot_asyncio.py:
--------------------------------------------------------------------------------
1 | """
2 | microdot_asyncio
3 | ----------------
4 |
5 | The ``microdot_asyncio`` module defines a few classes that help implement
6 | HTTP-based servers for MicroPython and standard Python that use ``asyncio``
7 | and coroutines.
8 | """
9 | try:
10 | import uasyncio as asyncio
11 | except ImportError:
12 | import asyncio
13 |
14 | try:
15 | import uio as io
16 | except ImportError:
17 | import io
18 |
19 | from microdot import Microdot as BaseMicrodot
20 | from microdot import mro
21 | from microdot import NoCaseDict
22 | from microdot import Request as BaseRequest
23 | from microdot import Response as BaseResponse
24 | from microdot import print_exception
25 | from microdot import HTTPException
26 | from microdot import MUTED_SOCKET_ERRORS
27 |
28 |
29 | def _iscoroutine(coro):
30 | return hasattr(coro, 'send') and hasattr(coro, 'throw')
31 |
32 |
33 | class _AsyncBytesIO:
34 | def __init__(self, data):
35 | self.stream = io.BytesIO(data)
36 |
37 | async def read(self, n=-1):
38 | return self.stream.read(n)
39 |
40 | async def readline(self): # pragma: no cover
41 | return self.stream.readline()
42 |
43 | async def readexactly(self, n): # pragma: no cover
44 | return self.stream.read(n)
45 |
46 | async def readuntil(self, separator=b'\n'): # pragma: no cover
47 | return self.stream.readuntil(separator=separator)
48 |
49 | async def awrite(self, data): # pragma: no cover
50 | return self.stream.write(data)
51 |
52 | async def aclose(self): # pragma: no cover
53 | pass
54 |
55 |
56 | class Request(BaseRequest):
57 | @staticmethod
58 | async def create(app, client_reader, client_writer, client_addr):
59 | """Create a request object.
60 |
61 | :param app: The Microdot application instance.
62 | :param client_reader: An input stream from where the request data can
63 | be read.
64 | :param client_writer: An output stream where the response data can be
65 | written.
66 | :param client_addr: The address of the client, as a tuple.
67 |
68 | This method is a coroutine. It returns a newly created ``Request``
69 | object.
70 | """
71 | # request line
72 | line = (await Request._safe_readline(client_reader)).strip().decode()
73 | if not line:
74 | return None
75 | method, url, http_version = line.split()
76 | http_version = http_version.split('/', 1)[1]
77 |
78 | # headers
79 | headers = NoCaseDict()
80 | content_length = 0
81 | while True:
82 | line = (await Request._safe_readline(
83 | client_reader)).strip().decode()
84 | if line == '':
85 | break
86 | header, value = line.split(':', 1)
87 | value = value.strip()
88 | headers[header] = value
89 | if header.lower() == 'content-length':
90 | content_length = int(value)
91 |
92 | # body
93 | body = b''
94 | if content_length and content_length <= Request.max_body_length:
95 | body = await client_reader.readexactly(content_length)
96 | stream = None
97 | else:
98 | body = b''
99 | stream = client_reader
100 |
101 | return Request(app, client_addr, method, url, http_version, headers,
102 | body=body, stream=stream,
103 | sock=(client_reader, client_writer))
104 |
105 | @property
106 | def stream(self):
107 | if self._stream is None:
108 | self._stream = _AsyncBytesIO(self._body)
109 | return self._stream
110 |
111 | @staticmethod
112 | async def _safe_readline(stream):
113 | line = (await stream.readline())
114 | if len(line) > Request.max_readline:
115 | raise ValueError('line too long')
116 | return line
117 |
118 |
119 | class Response(BaseResponse):
120 | """An HTTP response class.
121 |
122 | :param body: The body of the response. If a dictionary or list is given,
123 | a JSON formatter is used to generate the body. If a file-like
124 | object or an async generator is given, a streaming response is
125 | used. If a string is given, it is encoded from UTF-8. Else,
126 | the body should be a byte sequence.
127 | :param status_code: The numeric HTTP status code of the response. The
128 | default is 200.
129 | :param headers: A dictionary of headers to include in the response.
130 | :param reason: A custom reason phrase to add after the status code. The
131 | default is "OK" for responses with a 200 status code and
132 | "N/A" for any other status codes.
133 | """
134 |
135 | async def write(self, stream):
136 | self.complete()
137 |
138 | try:
139 | # status code
140 | reason = self.reason if self.reason is not None else \
141 | ('OK' if self.status_code == 200 else 'N/A')
142 | await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
143 | status_code=self.status_code, reason=reason).encode())
144 |
145 | # headers
146 | for header, value in self.headers.items():
147 | values = value if isinstance(value, list) else [value]
148 | for value in values:
149 | await stream.awrite('{header}: {value}\r\n'.format(
150 | header=header, value=value).encode())
151 | await stream.awrite(b'\r\n')
152 |
153 | # body
154 | async for body in self.body_iter():
155 | if isinstance(body, str): # pragma: no cover
156 | body = body.encode()
157 | await stream.awrite(body)
158 | except OSError as exc: # pragma: no cover
159 | if exc.errno in MUTED_SOCKET_ERRORS or \
160 | exc.args[0] == 'Connection lost':
161 | pass
162 | else:
163 | raise
164 |
165 | def body_iter(self):
166 | if hasattr(self.body, '__anext__'):
167 | # response body is an async generator
168 | return self.body
169 |
170 | response = self
171 |
172 | class iter:
173 | def __aiter__(self):
174 | if response.body:
175 | self.i = 0 # need to determine type of response.body
176 | else:
177 | self.i = -1 # no response body
178 | return self
179 |
180 | async def __anext__(self):
181 | if self.i == -1:
182 | raise StopAsyncIteration
183 | if self.i == 0:
184 | if hasattr(response.body, 'read'):
185 | self.i = 2 # response body is a file-like object
186 | elif hasattr(response.body, '__next__'):
187 | self.i = 1 # response body is a sync generator
188 | return next(response.body)
189 | else:
190 | self.i = -1 # response body is a plain string
191 | return response.body
192 | elif self.i == 1:
193 | try:
194 | return next(response.body)
195 | except StopIteration:
196 | raise StopAsyncIteration
197 | buf = response.body.read(response.send_file_buffer_size)
198 | if _iscoroutine(buf): # pragma: no cover
199 | buf = await buf
200 | if len(buf) < response.send_file_buffer_size:
201 | self.i = -1
202 | if hasattr(response.body, 'close'): # pragma: no cover
203 | result = response.body.close()
204 | if _iscoroutine(result):
205 | await result
206 | return buf
207 |
208 | return iter()
209 |
210 |
211 | class Microdot(BaseMicrodot):
212 | async def start_server(self, host='0.0.0.0', port=5000, debug=False,
213 | ssl=None):
214 | """Start the Microdot web server as a coroutine. This coroutine does
215 | not normally return, as the server enters an endless listening loop.
216 | The :func:`shutdown` function provides a method for terminating the
217 | server gracefully.
218 |
219 | :param host: The hostname or IP address of the network interface that
220 | will be listening for requests. A value of ``'0.0.0.0'``
221 | (the default) indicates that the server should listen for
222 | requests on all the available interfaces, and a value of
223 | ``127.0.0.1`` indicates that the server should listen
224 | for requests only on the internal networking interface of
225 | the host.
226 | :param port: The port number to listen for requests. The default is
227 | port 5000.
228 | :param debug: If ``True``, the server logs debugging information. The
229 | default is ``False``.
230 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
231 | not use TLS. The default is ``None``.
232 |
233 | This method is a coroutine.
234 |
235 | Example::
236 |
237 | import asyncio
238 | from microdot_asyncio import Microdot
239 |
240 | app = Microdot()
241 |
242 | @app.route('/')
243 | async def index():
244 | return 'Hello, world!'
245 |
246 | async def main():
247 | await app.start_server(debug=True)
248 |
249 | asyncio.run(main())
250 | """
251 | self.debug = debug
252 |
253 | async def serve(reader, writer):
254 | if not hasattr(writer, 'awrite'): # pragma: no cover
255 | # CPython provides the awrite and aclose methods in 3.8+
256 | async def awrite(self, data):
257 | self.write(data)
258 | await self.drain()
259 |
260 | async def aclose(self):
261 | self.close()
262 | await self.wait_closed()
263 |
264 | from types import MethodType
265 | writer.awrite = MethodType(awrite, writer)
266 | writer.aclose = MethodType(aclose, writer)
267 |
268 | await self.handle_request(reader, writer)
269 |
270 | if self.debug: # pragma: no cover
271 | print('Starting async server on {host}:{port}...'.format(
272 | host=host, port=port))
273 |
274 | try:
275 | self.server = await asyncio.start_server(serve, host, port,
276 | ssl=ssl)
277 | except TypeError:
278 | self.server = await asyncio.start_server(serve, host, port)
279 |
280 | while True:
281 | try:
282 | await self.server.wait_closed()
283 | break
284 | except AttributeError: # pragma: no cover
285 | # the task hasn't been initialized in the server object yet
286 | # wait a bit and try again
287 | await asyncio.sleep(0.1)
288 |
289 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
290 | """Start the web server. This function does not normally return, as
291 | the server enters an endless listening loop. The :func:`shutdown`
292 | function provides a method for terminating the server gracefully.
293 |
294 | :param host: The hostname or IP address of the network interface that
295 | will be listening for requests. A value of ``'0.0.0.0'``
296 | (the default) indicates that the server should listen for
297 | requests on all the available interfaces, and a value of
298 | ``127.0.0.1`` indicates that the server should listen
299 | for requests only on the internal networking interface of
300 | the host.
301 | :param port: The port number to listen for requests. The default is
302 | port 5000.
303 | :param debug: If ``True``, the server logs debugging information. The
304 | default is ``False``.
305 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
306 | not use TLS. The default is ``None``.
307 |
308 | Example::
309 |
310 | from microdot_asyncio import Microdot
311 |
312 | app = Microdot()
313 |
314 | @app.route('/')
315 | async def index():
316 | return 'Hello, world!'
317 |
318 | app.run(debug=True)
319 | """
320 | asyncio.run(self.start_server(host=host, port=port, debug=debug,
321 | ssl=ssl))
322 |
323 | def shutdown(self):
324 | self.server.close()
325 |
326 | async def handle_request(self, reader, writer):
327 | req = None
328 | try:
329 | req = await Request.create(self, reader, writer,
330 | writer.get_extra_info('peername'))
331 | except Exception as exc: # pragma: no cover
332 | print_exception(exc)
333 |
334 | res = await self.dispatch_request(req)
335 | if res != Response.already_handled: # pragma: no branch
336 | await res.write(writer)
337 | try:
338 | await writer.aclose()
339 | except OSError as exc: # pragma: no cover
340 | if exc.errno in MUTED_SOCKET_ERRORS:
341 | pass
342 | else:
343 | raise
344 | if self.debug and req: # pragma: no cover
345 | print('{method} {path} {status_code}'.format(
346 | method=req.method, path=req.path,
347 | status_code=res.status_code))
348 |
349 | async def dispatch_request(self, req):
350 | after_request_handled = False
351 | if req:
352 | if req.content_length > req.max_content_length:
353 | if 413 in self.error_handlers:
354 | res = await self._invoke_handler(
355 | self.error_handlers[413], req)
356 | else:
357 | res = 'Payload too large', 413
358 | else:
359 | f = self.find_route(req)
360 | try:
361 | res = None
362 | if callable(f):
363 | for handler in self.before_request_handlers:
364 | res = await self._invoke_handler(handler, req)
365 | if res:
366 | break
367 | if res is None:
368 | res = await self._invoke_handler(
369 | f, req, **req.url_args)
370 | if isinstance(res, tuple):
371 | body = res[0]
372 | if isinstance(res[1], int):
373 | status_code = res[1]
374 | headers = res[2] if len(res) > 2 else {}
375 | else:
376 | status_code = 200
377 | headers = res[1]
378 | res = Response(body, status_code, headers)
379 | elif not isinstance(res, Response):
380 | res = Response(res)
381 | for handler in self.after_request_handlers:
382 | res = await self._invoke_handler(
383 | handler, req, res) or res
384 | for handler in req.after_request_handlers:
385 | res = await self._invoke_handler(
386 | handler, req, res) or res
387 | after_request_handled = True
388 | elif f in self.error_handlers:
389 | res = await self._invoke_handler(
390 | self.error_handlers[f], req)
391 | else:
392 | res = 'Not found', f
393 | except HTTPException as exc:
394 | if exc.status_code in self.error_handlers:
395 | res = self.error_handlers[exc.status_code](req)
396 | else:
397 | res = exc.reason, exc.status_code
398 | except Exception as exc:
399 | print_exception(exc)
400 | exc_class = None
401 | res = None
402 | if exc.__class__ in self.error_handlers:
403 | exc_class = exc.__class__
404 | else:
405 | for c in mro(exc.__class__)[1:]:
406 | if c in self.error_handlers:
407 | exc_class = c
408 | break
409 | if exc_class:
410 | try:
411 | res = await self._invoke_handler(
412 | self.error_handlers[exc_class], req, exc)
413 | except Exception as exc2: # pragma: no cover
414 | print_exception(exc2)
415 | if res is None:
416 | if 500 in self.error_handlers:
417 | res = await self._invoke_handler(
418 | self.error_handlers[500], req)
419 | else:
420 | res = 'Internal server error', 500
421 | else:
422 | if 400 in self.error_handlers:
423 | res = await self._invoke_handler(self.error_handlers[400], req)
424 | else:
425 | res = 'Bad request', 400
426 | if isinstance(res, tuple):
427 | res = Response(*res)
428 | elif not isinstance(res, Response):
429 | res = Response(res)
430 | if not after_request_handled:
431 | for handler in self.after_error_request_handlers:
432 | res = await self._invoke_handler(
433 | handler, req, res) or res
434 | return res
435 |
436 | async def _invoke_handler(self, f_or_coro, *args, **kwargs):
437 | ret = f_or_coro(*args, **kwargs)
438 | if _iscoroutine(ret):
439 | ret = await ret
440 | return ret
441 |
442 |
443 | abort = Microdot.abort
444 | Response.already_handled = Response()
445 | redirect = Response.redirect
446 | send_file = Response.send_file
447 |
--------------------------------------------------------------------------------
/control-components-from-web-server/microdot_asyncio.py:
--------------------------------------------------------------------------------
1 | """
2 | microdot_asyncio
3 | ----------------
4 |
5 | The ``microdot_asyncio`` module defines a few classes that help implement
6 | HTTP-based servers for MicroPython and standard Python that use ``asyncio``
7 | and coroutines.
8 | """
9 | try:
10 | import uasyncio as asyncio
11 | except ImportError:
12 | import asyncio
13 |
14 | try:
15 | import uio as io
16 | except ImportError:
17 | import io
18 |
19 | from microdot import Microdot as BaseMicrodot
20 | from microdot import mro
21 | from microdot import NoCaseDict
22 | from microdot import Request as BaseRequest
23 | from microdot import Response as BaseResponse
24 | from microdot import print_exception
25 | from microdot import HTTPException
26 | from microdot import MUTED_SOCKET_ERRORS
27 |
28 |
29 | def _iscoroutine(coro):
30 | return hasattr(coro, 'send') and hasattr(coro, 'throw')
31 |
32 |
33 | class _AsyncBytesIO:
34 | def __init__(self, data):
35 | self.stream = io.BytesIO(data)
36 |
37 | async def read(self, n=-1):
38 | return self.stream.read(n)
39 |
40 | async def readline(self): # pragma: no cover
41 | return self.stream.readline()
42 |
43 | async def readexactly(self, n): # pragma: no cover
44 | return self.stream.read(n)
45 |
46 | async def readuntil(self, separator=b'\n'): # pragma: no cover
47 | return self.stream.readuntil(separator=separator)
48 |
49 | async def awrite(self, data): # pragma: no cover
50 | return self.stream.write(data)
51 |
52 | async def aclose(self): # pragma: no cover
53 | pass
54 |
55 |
56 | class Request(BaseRequest):
57 | @staticmethod
58 | async def create(app, client_reader, client_writer, client_addr):
59 | """Create a request object.
60 |
61 | :param app: The Microdot application instance.
62 | :param client_reader: An input stream from where the request data can
63 | be read.
64 | :param client_writer: An output stream where the response data can be
65 | written.
66 | :param client_addr: The address of the client, as a tuple.
67 |
68 | This method is a coroutine. It returns a newly created ``Request``
69 | object.
70 | """
71 | # request line
72 | line = (await Request._safe_readline(client_reader)).strip().decode()
73 | if not line:
74 | return None
75 | method, url, http_version = line.split()
76 | http_version = http_version.split('/', 1)[1]
77 |
78 | # headers
79 | headers = NoCaseDict()
80 | content_length = 0
81 | while True:
82 | line = (await Request._safe_readline(
83 | client_reader)).strip().decode()
84 | if line == '':
85 | break
86 | header, value = line.split(':', 1)
87 | value = value.strip()
88 | headers[header] = value
89 | if header.lower() == 'content-length':
90 | content_length = int(value)
91 |
92 | # body
93 | body = b''
94 | if content_length and content_length <= Request.max_body_length:
95 | body = await client_reader.readexactly(content_length)
96 | stream = None
97 | else:
98 | body = b''
99 | stream = client_reader
100 |
101 | return Request(app, client_addr, method, url, http_version, headers,
102 | body=body, stream=stream,
103 | sock=(client_reader, client_writer))
104 |
105 | @property
106 | def stream(self):
107 | if self._stream is None:
108 | self._stream = _AsyncBytesIO(self._body)
109 | return self._stream
110 |
111 | @staticmethod
112 | async def _safe_readline(stream):
113 | line = (await stream.readline())
114 | if len(line) > Request.max_readline:
115 | raise ValueError('line too long')
116 | return line
117 |
118 |
119 | class Response(BaseResponse):
120 | """An HTTP response class.
121 |
122 | :param body: The body of the response. If a dictionary or list is given,
123 | a JSON formatter is used to generate the body. If a file-like
124 | object or an async generator is given, a streaming response is
125 | used. If a string is given, it is encoded from UTF-8. Else,
126 | the body should be a byte sequence.
127 | :param status_code: The numeric HTTP status code of the response. The
128 | default is 200.
129 | :param headers: A dictionary of headers to include in the response.
130 | :param reason: A custom reason phrase to add after the status code. The
131 | default is "OK" for responses with a 200 status code and
132 | "N/A" for any other status codes.
133 | """
134 |
135 | async def write(self, stream):
136 | self.complete()
137 |
138 | try:
139 | # status code
140 | reason = self.reason if self.reason is not None else \
141 | ('OK' if self.status_code == 200 else 'N/A')
142 | await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
143 | status_code=self.status_code, reason=reason).encode())
144 |
145 | # headers
146 | for header, value in self.headers.items():
147 | values = value if isinstance(value, list) else [value]
148 | for value in values:
149 | await stream.awrite('{header}: {value}\r\n'.format(
150 | header=header, value=value).encode())
151 | await stream.awrite(b'\r\n')
152 |
153 | # body
154 | async for body in self.body_iter():
155 | if isinstance(body, str): # pragma: no cover
156 | body = body.encode()
157 | await stream.awrite(body)
158 | except OSError as exc: # pragma: no cover
159 | if exc.errno in MUTED_SOCKET_ERRORS or \
160 | exc.args[0] == 'Connection lost':
161 | pass
162 | else:
163 | raise
164 |
165 | def body_iter(self):
166 | if hasattr(self.body, '__anext__'):
167 | # response body is an async generator
168 | return self.body
169 |
170 | response = self
171 |
172 | class iter:
173 | def __aiter__(self):
174 | if response.body:
175 | self.i = 0 # need to determine type of response.body
176 | else:
177 | self.i = -1 # no response body
178 | return self
179 |
180 | async def __anext__(self):
181 | if self.i == -1:
182 | raise StopAsyncIteration
183 | if self.i == 0:
184 | if hasattr(response.body, 'read'):
185 | self.i = 2 # response body is a file-like object
186 | elif hasattr(response.body, '__next__'):
187 | self.i = 1 # response body is a sync generator
188 | return next(response.body)
189 | else:
190 | self.i = -1 # response body is a plain string
191 | return response.body
192 | elif self.i == 1:
193 | try:
194 | return next(response.body)
195 | except StopIteration:
196 | raise StopAsyncIteration
197 | buf = response.body.read(response.send_file_buffer_size)
198 | if _iscoroutine(buf): # pragma: no cover
199 | buf = await buf
200 | if len(buf) < response.send_file_buffer_size:
201 | self.i = -1
202 | if hasattr(response.body, 'close'): # pragma: no cover
203 | result = response.body.close()
204 | if _iscoroutine(result):
205 | await result
206 | return buf
207 |
208 | return iter()
209 |
210 |
211 | class Microdot(BaseMicrodot):
212 | async def start_server(self, host='0.0.0.0', port=5000, debug=False,
213 | ssl=None):
214 | """Start the Microdot web server as a coroutine. This coroutine does
215 | not normally return, as the server enters an endless listening loop.
216 | The :func:`shutdown` function provides a method for terminating the
217 | server gracefully.
218 |
219 | :param host: The hostname or IP address of the network interface that
220 | will be listening for requests. A value of ``'0.0.0.0'``
221 | (the default) indicates that the server should listen for
222 | requests on all the available interfaces, and a value of
223 | ``127.0.0.1`` indicates that the server should listen
224 | for requests only on the internal networking interface of
225 | the host.
226 | :param port: The port number to listen for requests. The default is
227 | port 5000.
228 | :param debug: If ``True``, the server logs debugging information. The
229 | default is ``False``.
230 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
231 | not use TLS. The default is ``None``.
232 |
233 | This method is a coroutine.
234 |
235 | Example::
236 |
237 | import asyncio
238 | from microdot_asyncio import Microdot
239 |
240 | app = Microdot()
241 |
242 | @app.route('/')
243 | async def index():
244 | return 'Hello, world!'
245 |
246 | async def main():
247 | await app.start_server(debug=True)
248 |
249 | asyncio.run(main())
250 | """
251 | self.debug = debug
252 |
253 | async def serve(reader, writer):
254 | if not hasattr(writer, 'awrite'): # pragma: no cover
255 | # CPython provides the awrite and aclose methods in 3.8+
256 | async def awrite(self, data):
257 | self.write(data)
258 | await self.drain()
259 |
260 | async def aclose(self):
261 | self.close()
262 | await self.wait_closed()
263 |
264 | from types import MethodType
265 | writer.awrite = MethodType(awrite, writer)
266 | writer.aclose = MethodType(aclose, writer)
267 |
268 | await self.handle_request(reader, writer)
269 |
270 | if self.debug: # pragma: no cover
271 | print('Starting async server on {host}:{port}...'.format(
272 | host=host, port=port))
273 |
274 | try:
275 | self.server = await asyncio.start_server(serve, host, port,
276 | ssl=ssl)
277 | except TypeError:
278 | self.server = await asyncio.start_server(serve, host, port)
279 |
280 | while True:
281 | try:
282 | await self.server.wait_closed()
283 | break
284 | except AttributeError: # pragma: no cover
285 | # the task hasn't been initialized in the server object yet
286 | # wait a bit and try again
287 | await asyncio.sleep(0.1)
288 |
289 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
290 | """Start the web server. This function does not normally return, as
291 | the server enters an endless listening loop. The :func:`shutdown`
292 | function provides a method for terminating the server gracefully.
293 |
294 | :param host: The hostname or IP address of the network interface that
295 | will be listening for requests. A value of ``'0.0.0.0'``
296 | (the default) indicates that the server should listen for
297 | requests on all the available interfaces, and a value of
298 | ``127.0.0.1`` indicates that the server should listen
299 | for requests only on the internal networking interface of
300 | the host.
301 | :param port: The port number to listen for requests. The default is
302 | port 5000.
303 | :param debug: If ``True``, the server logs debugging information. The
304 | default is ``False``.
305 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
306 | not use TLS. The default is ``None``.
307 |
308 | Example::
309 |
310 | from microdot_asyncio import Microdot
311 |
312 | app = Microdot()
313 |
314 | @app.route('/')
315 | async def index():
316 | return 'Hello, world!'
317 |
318 | app.run(debug=True)
319 | """
320 | asyncio.run(self.start_server(host=host, port=port, debug=debug,
321 | ssl=ssl))
322 |
323 | def shutdown(self):
324 | self.server.close()
325 |
326 | async def handle_request(self, reader, writer):
327 | req = None
328 | try:
329 | req = await Request.create(self, reader, writer,
330 | writer.get_extra_info('peername'))
331 | except Exception as exc: # pragma: no cover
332 | print_exception(exc)
333 |
334 | res = await self.dispatch_request(req)
335 | if res != Response.already_handled: # pragma: no branch
336 | await res.write(writer)
337 | try:
338 | await writer.aclose()
339 | except OSError as exc: # pragma: no cover
340 | if exc.errno in MUTED_SOCKET_ERRORS:
341 | pass
342 | else:
343 | raise
344 | if self.debug and req: # pragma: no cover
345 | print('{method} {path} {status_code}'.format(
346 | method=req.method, path=req.path,
347 | status_code=res.status_code))
348 |
349 | async def dispatch_request(self, req):
350 | after_request_handled = False
351 | if req:
352 | if req.content_length > req.max_content_length:
353 | if 413 in self.error_handlers:
354 | res = await self._invoke_handler(
355 | self.error_handlers[413], req)
356 | else:
357 | res = 'Payload too large', 413
358 | else:
359 | f = self.find_route(req)
360 | try:
361 | res = None
362 | if callable(f):
363 | for handler in self.before_request_handlers:
364 | res = await self._invoke_handler(handler, req)
365 | if res:
366 | break
367 | if res is None:
368 | res = await self._invoke_handler(
369 | f, req, **req.url_args)
370 | if isinstance(res, tuple):
371 | body = res[0]
372 | if isinstance(res[1], int):
373 | status_code = res[1]
374 | headers = res[2] if len(res) > 2 else {}
375 | else:
376 | status_code = 200
377 | headers = res[1]
378 | res = Response(body, status_code, headers)
379 | elif not isinstance(res, Response):
380 | res = Response(res)
381 | for handler in self.after_request_handlers:
382 | res = await self._invoke_handler(
383 | handler, req, res) or res
384 | for handler in req.after_request_handlers:
385 | res = await self._invoke_handler(
386 | handler, req, res) or res
387 | after_request_handled = True
388 | elif f in self.error_handlers:
389 | res = await self._invoke_handler(
390 | self.error_handlers[f], req)
391 | else:
392 | res = 'Not found', f
393 | except HTTPException as exc:
394 | if exc.status_code in self.error_handlers:
395 | res = self.error_handlers[exc.status_code](req)
396 | else:
397 | res = exc.reason, exc.status_code
398 | except Exception as exc:
399 | print_exception(exc)
400 | exc_class = None
401 | res = None
402 | if exc.__class__ in self.error_handlers:
403 | exc_class = exc.__class__
404 | else:
405 | for c in mro(exc.__class__)[1:]:
406 | if c in self.error_handlers:
407 | exc_class = c
408 | break
409 | if exc_class:
410 | try:
411 | res = await self._invoke_handler(
412 | self.error_handlers[exc_class], req, exc)
413 | except Exception as exc2: # pragma: no cover
414 | print_exception(exc2)
415 | if res is None:
416 | if 500 in self.error_handlers:
417 | res = await self._invoke_handler(
418 | self.error_handlers[500], req)
419 | else:
420 | res = 'Internal server error', 500
421 | else:
422 | if 400 in self.error_handlers:
423 | res = await self._invoke_handler(self.error_handlers[400], req)
424 | else:
425 | res = 'Bad request', 400
426 | if isinstance(res, tuple):
427 | res = Response(*res)
428 | elif not isinstance(res, Response):
429 | res = Response(res)
430 | if not after_request_handled:
431 | for handler in self.after_error_request_handlers:
432 | res = await self._invoke_handler(
433 | handler, req, res) or res
434 | return res
435 |
436 | async def _invoke_handler(self, f_or_coro, *args, **kwargs):
437 | ret = f_or_coro(*args, **kwargs)
438 | if _iscoroutine(ret):
439 | ret = await ret
440 | return ret
441 |
442 |
443 | abort = Microdot.abort
444 | Response.already_handled = Response()
445 | redirect = Response.redirect
446 | send_file = Response.send_file
447 |
--------------------------------------------------------------------------------
/microdot-dynamic-component-path/microdot.py:
--------------------------------------------------------------------------------
1 | """
2 | microdot
3 | --------
4 |
5 | The ``microdot`` module defines a few classes that help implement HTTP-based
6 | servers for MicroPython and standard Python, with multithreading support for
7 | Python interpreters that support it.
8 | """
9 | try:
10 | from sys import print_exception
11 | except ImportError: # pragma: no cover
12 | import traceback
13 |
14 | def print_exception(exc):
15 | traceback.print_exc()
16 | try:
17 | import uerrno as errno
18 | except ImportError:
19 | import errno
20 |
21 | concurrency_mode = 'threaded'
22 |
23 | try: # pragma: no cover
24 | import threading
25 |
26 | def create_thread(f, *args, **kwargs):
27 | # use the threading module
28 | threading.Thread(target=f, args=args, kwargs=kwargs).start()
29 | except ImportError: # pragma: no cover
30 | def create_thread(f, *args, **kwargs):
31 | # no threads available, call function synchronously
32 | f(*args, **kwargs)
33 |
34 | concurrency_mode = 'sync'
35 |
36 | try:
37 | import ujson as json
38 | except ImportError:
39 | import json
40 |
41 | try:
42 | import ure as re
43 | except ImportError:
44 | import re
45 |
46 | try:
47 | import usocket as socket
48 | except ImportError:
49 | try:
50 | import socket
51 | except ImportError: # pragma: no cover
52 | socket = None
53 |
54 | MUTED_SOCKET_ERRORS = [
55 | 32, # Broken pipe
56 | 54, # Connection reset by peer
57 | 104, # Connection reset by peer
58 | 128, # Operation on closed socket
59 | ]
60 |
61 |
62 | def urldecode_str(s):
63 | s = s.replace('+', ' ')
64 | parts = s.split('%')
65 | if len(parts) == 1:
66 | return s
67 | result = [parts[0]]
68 | for item in parts[1:]:
69 | if item == '':
70 | result.append('%')
71 | else:
72 | code = item[:2]
73 | result.append(chr(int(code, 16)))
74 | result.append(item[2:])
75 | return ''.join(result)
76 |
77 |
78 | def urldecode_bytes(s):
79 | s = s.replace(b'+', b' ')
80 | parts = s.split(b'%')
81 | if len(parts) == 1:
82 | return s.decode()
83 | result = [parts[0]]
84 | for item in parts[1:]:
85 | if item == b'':
86 | result.append(b'%')
87 | else:
88 | code = item[:2]
89 | result.append(bytes([int(code, 16)]))
90 | result.append(item[2:])
91 | return b''.join(result).decode()
92 |
93 |
94 | def urlencode(s):
95 | return s.replace('+', '%2B').replace(' ', '+').replace(
96 | '%', '%25').replace('?', '%3F').replace('#', '%23').replace(
97 | '&', '%26').replace('=', '%3D')
98 |
99 |
100 | class NoCaseDict(dict):
101 | """A subclass of dictionary that holds case-insensitive keys.
102 |
103 | :param initial_dict: an initial dictionary of key/value pairs to
104 | initialize this object with.
105 |
106 | Example::
107 |
108 | >>> d = NoCaseDict()
109 | >>> d['Content-Type'] = 'text/html'
110 | >>> print(d['Content-Type'])
111 | text/html
112 | >>> print(d['content-type'])
113 | text/html
114 | >>> print(d['CONTENT-TYPE'])
115 | text/html
116 | >>> del d['cOnTeNt-TyPe']
117 | >>> print(d)
118 | {}
119 | """
120 | def __init__(self, initial_dict=None):
121 | super().__init__(initial_dict or {})
122 | self.keymap = {k.lower(): k for k in self.keys() if k.lower() != k}
123 |
124 | def __setitem__(self, key, value):
125 | kl = key.lower()
126 | key = self.keymap.get(kl, key)
127 | if kl != key:
128 | self.keymap[kl] = key
129 | super().__setitem__(key, value)
130 |
131 | def __getitem__(self, key):
132 | kl = key.lower()
133 | return super().__getitem__(self.keymap.get(kl, kl))
134 |
135 | def __delitem__(self, key):
136 | kl = key.lower()
137 | super().__delitem__(self.keymap.get(kl, kl))
138 |
139 | def __contains__(self, key):
140 | kl = key.lower()
141 | return self.keymap.get(kl, kl) in self.keys()
142 |
143 | def get(self, key, default=None):
144 | kl = key.lower()
145 | return super().get(self.keymap.get(kl, kl), default)
146 |
147 |
148 | def mro(cls): # pragma: no cover
149 | """Return the method resolution order of a class.
150 |
151 | This is a helper function that returns the method resolution order of a
152 | class. It is used by Microdot to find the best error handler to invoke for
153 | the raised exception.
154 |
155 | In CPython, this function returns the ``__mro__`` attribute of the class.
156 | In MicroPython, this function implements a recursive depth-first scanning
157 | of the class hierarchy.
158 | """
159 | if hasattr(cls, 'mro'):
160 | return cls.__mro__
161 |
162 | def _mro(cls):
163 | m = [cls]
164 | for base in cls.__bases__:
165 | m += _mro(base)
166 | return m
167 |
168 | mro_list = _mro(cls)
169 |
170 | # If a class appears multiple times (due to multiple inheritance) remove
171 | # all but the last occurence. This matches the method resolution order
172 | # of MicroPython, but not CPython.
173 | mro_pruned = []
174 | for i in range(len(mro_list)):
175 | base = mro_list.pop(0)
176 | if base not in mro_list:
177 | mro_pruned.append(base)
178 | return mro_pruned
179 |
180 |
181 | class MultiDict(dict):
182 | """A subclass of dictionary that can hold multiple values for the same
183 | key. It is used to hold key/value pairs decoded from query strings and
184 | form submissions.
185 |
186 | :param initial_dict: an initial dictionary of key/value pairs to
187 | initialize this object with.
188 |
189 | Example::
190 |
191 | >>> d = MultiDict()
192 | >>> d['sort'] = 'name'
193 | >>> d['sort'] = 'email'
194 | >>> print(d['sort'])
195 | 'name'
196 | >>> print(d.getlist('sort'))
197 | ['name', 'email']
198 | """
199 | def __init__(self, initial_dict=None):
200 | super().__init__()
201 | if initial_dict:
202 | for key, value in initial_dict.items():
203 | self[key] = value
204 |
205 | def __setitem__(self, key, value):
206 | if key not in self:
207 | super().__setitem__(key, [])
208 | super().__getitem__(key).append(value)
209 |
210 | def __getitem__(self, key):
211 | return super().__getitem__(key)[0]
212 |
213 | def get(self, key, default=None, type=None):
214 | """Return the value for a given key.
215 |
216 | :param key: The key to retrieve.
217 | :param default: A default value to use if the key does not exist.
218 | :param type: A type conversion callable to apply to the value.
219 |
220 | If the multidict contains more than one value for the requested key,
221 | this method returns the first value only.
222 |
223 | Example::
224 |
225 | >>> d = MultiDict()
226 | >>> d['age'] = '42'
227 | >>> d.get('age')
228 | '42'
229 | >>> d.get('age', type=int)
230 | 42
231 | >>> d.get('name', default='noname')
232 | 'noname'
233 | """
234 | if key not in self:
235 | return default
236 | value = self[key]
237 | if type is not None:
238 | value = type(value)
239 | return value
240 |
241 | def getlist(self, key, type=None):
242 | """Return all the values for a given key.
243 |
244 | :param key: The key to retrieve.
245 | :param type: A type conversion callable to apply to the values.
246 |
247 | If the requested key does not exist in the dictionary, this method
248 | returns an empty list.
249 |
250 | Example::
251 |
252 | >>> d = MultiDict()
253 | >>> d.getlist('items')
254 | []
255 | >>> d['items'] = '3'
256 | >>> d.getlist('items')
257 | ['3']
258 | >>> d['items'] = '56'
259 | >>> d.getlist('items')
260 | ['3', '56']
261 | >>> d.getlist('items', type=int)
262 | [3, 56]
263 | """
264 | if key not in self:
265 | return []
266 | values = super().__getitem__(key)
267 | if type is not None:
268 | values = [type(value) for value in values]
269 | return values
270 |
271 |
272 | class Request():
273 | """An HTTP request."""
274 | #: Specify the maximum payload size that is accepted. Requests with larger
275 | #: payloads will be rejected with a 413 status code. Applications can
276 | #: change this maximum as necessary.
277 | #:
278 | #: Example::
279 | #:
280 | #: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
281 | max_content_length = 16 * 1024
282 |
283 | #: Specify the maximum payload size that can be stored in ``body``.
284 | #: Requests with payloads that are larger than this size and up to
285 | #: ``max_content_length`` bytes will be accepted, but the application will
286 | #: only be able to access the body of the request by reading from
287 | #: ``stream``. Set to 0 if you always access the body as a stream.
288 | #:
289 | #: Example::
290 | #:
291 | #: Request.max_body_length = 4 * 1024 # up to 4KB bodies read
292 | max_body_length = 16 * 1024
293 |
294 | #: Specify the maximum length allowed for a line in the request. Requests
295 | #: with longer lines will not be correctly interpreted. Applications can
296 | #: change this maximum as necessary.
297 | #:
298 | #: Example::
299 | #:
300 | #: Request.max_readline = 16 * 1024 # 16KB lines allowed
301 | max_readline = 2 * 1024
302 |
303 | class G:
304 | pass
305 |
306 | def __init__(self, app, client_addr, method, url, http_version, headers,
307 | body=None, stream=None, sock=None):
308 | #: The application instance to which this request belongs.
309 | self.app = app
310 | #: The address of the client, as a tuple (host, port).
311 | self.client_addr = client_addr
312 | #: The HTTP method of the request.
313 | self.method = method
314 | #: The request URL, including the path and query string.
315 | self.url = url
316 | #: The path portion of the URL.
317 | self.path = url
318 | #: The query string portion of the URL.
319 | self.query_string = None
320 | #: The parsed query string, as a
321 | #: :class:`MultiDict ` object.
322 | self.args = {}
323 | #: A dictionary with the headers included in the request.
324 | self.headers = headers
325 | #: A dictionary with the cookies included in the request.
326 | self.cookies = {}
327 | #: The parsed ``Content-Length`` header.
328 | self.content_length = 0
329 | #: The parsed ``Content-Type`` header.
330 | self.content_type = None
331 | #: A general purpose container for applications to store data during
332 | #: the life of the request.
333 | self.g = Request.G()
334 |
335 | self.http_version = http_version
336 | if '?' in self.path:
337 | self.path, self.query_string = self.path.split('?', 1)
338 | self.args = self._parse_urlencoded(self.query_string)
339 |
340 | if 'Content-Length' in self.headers:
341 | self.content_length = int(self.headers['Content-Length'])
342 | if 'Content-Type' in self.headers:
343 | self.content_type = self.headers['Content-Type']
344 | if 'Cookie' in self.headers:
345 | for cookie in self.headers['Cookie'].split(';'):
346 | name, value = cookie.strip().split('=', 1)
347 | self.cookies[name] = value
348 |
349 | self._body = body
350 | self.body_used = False
351 | self._stream = stream
352 | self.stream_used = False
353 | self.sock = sock
354 | self._json = None
355 | self._form = None
356 | self.after_request_handlers = []
357 |
358 | @staticmethod
359 | def create(app, client_stream, client_addr, client_sock=None):
360 | """Create a request object.
361 |
362 |
363 | :param app: The Microdot application instance.
364 | :param client_stream: An input stream from where the request data can
365 | be read.
366 | :param client_addr: The address of the client, as a tuple.
367 | :param client_sock: The low-level socket associated with the request.
368 |
369 | This method returns a newly created ``Request`` object.
370 | """
371 | # request line
372 | line = Request._safe_readline(client_stream).strip().decode()
373 | if not line:
374 | return None
375 | method, url, http_version = line.split()
376 | http_version = http_version.split('/', 1)[1]
377 |
378 | # headers
379 | headers = NoCaseDict()
380 | while True:
381 | line = Request._safe_readline(client_stream).strip().decode()
382 | if line == '':
383 | break
384 | header, value = line.split(':', 1)
385 | value = value.strip()
386 | headers[header] = value
387 |
388 | return Request(app, client_addr, method, url, http_version, headers,
389 | stream=client_stream, sock=client_sock)
390 |
391 | def _parse_urlencoded(self, urlencoded):
392 | data = MultiDict()
393 | if len(urlencoded) > 0:
394 | if isinstance(urlencoded, str):
395 | for k, v in [pair.split('=', 1)
396 | for pair in urlencoded.split('&')]:
397 | data[urldecode_str(k)] = urldecode_str(v)
398 | elif isinstance(urlencoded, bytes): # pragma: no branch
399 | for k, v in [pair.split(b'=', 1)
400 | for pair in urlencoded.split(b'&')]:
401 | data[urldecode_bytes(k)] = urldecode_bytes(v)
402 | return data
403 |
404 | @property
405 | def body(self):
406 | """The body of the request, as bytes."""
407 | if self.stream_used:
408 | raise RuntimeError('Cannot use both stream and body')
409 | if self._body is None:
410 | self._body = b''
411 | if self.content_length and \
412 | self.content_length <= Request.max_body_length:
413 | while len(self._body) < self.content_length:
414 | data = self._stream.read(
415 | self.content_length - len(self._body))
416 | if len(data) == 0: # pragma: no cover
417 | raise EOFError()
418 | self._body += data
419 | self.body_used = True
420 | return self._body
421 |
422 | @property
423 | def stream(self):
424 | """The input stream, containing the request body."""
425 | if self.body_used:
426 | raise RuntimeError('Cannot use both stream and body')
427 | self.stream_used = True
428 | return self._stream
429 |
430 | @property
431 | def json(self):
432 | """The parsed JSON body, or ``None`` if the request does not have a
433 | JSON body."""
434 | if self._json is None:
435 | if self.content_type is None:
436 | return None
437 | mime_type = self.content_type.split(';')[0]
438 | if mime_type != 'application/json':
439 | return None
440 | self._json = json.loads(self.body.decode())
441 | return self._json
442 |
443 | @property
444 | def form(self):
445 | """The parsed form submission body, as a
446 | :class:`MultiDict ` object, or ``None`` if the
447 | request does not have a form submission."""
448 | if self._form is None:
449 | if self.content_type is None:
450 | return None
451 | mime_type = self.content_type.split(';')[0]
452 | if mime_type != 'application/x-www-form-urlencoded':
453 | return None
454 | self._form = self._parse_urlencoded(self.body)
455 | return self._form
456 |
457 | def after_request(self, f):
458 | """Register a request-specific function to run after the request is
459 | handled. Request-specific after request handlers run at the very end,
460 | after the application's own after request handlers. The function must
461 | take two arguments, the request and response objects. The return value
462 | of the function must be the updated response object.
463 |
464 | Example::
465 |
466 | @app.route('/')
467 | def index(request):
468 | # register a request-specific after request handler
469 | @req.after_request
470 | def func(request, response):
471 | # ...
472 | return response
473 |
474 | return 'Hello, World!'
475 | """
476 | self.after_request_handlers.append(f)
477 | return f
478 |
479 | @staticmethod
480 | def _safe_readline(stream):
481 | line = stream.readline(Request.max_readline + 1)
482 | if len(line) > Request.max_readline:
483 | raise ValueError('line too long')
484 | return line
485 |
486 |
487 | class Response():
488 | """An HTTP response class.
489 |
490 | :param body: The body of the response. If a dictionary or list is given,
491 | a JSON formatter is used to generate the body. If a file-like
492 | object or a generator is given, a streaming response is used.
493 | If a string is given, it is encoded from UTF-8. Else, the
494 | body should be a byte sequence.
495 | :param status_code: The numeric HTTP status code of the response. The
496 | default is 200.
497 | :param headers: A dictionary of headers to include in the response.
498 | :param reason: A custom reason phrase to add after the status code. The
499 | default is "OK" for responses with a 200 status code and
500 | "N/A" for any other status codes.
501 | """
502 | types_map = {
503 | 'css': 'text/css',
504 | 'gif': 'image/gif',
505 | 'html': 'text/html',
506 | 'jpg': 'image/jpeg',
507 | 'js': 'application/javascript',
508 | 'json': 'application/json',
509 | 'png': 'image/png',
510 | 'txt': 'text/plain',
511 | }
512 | send_file_buffer_size = 1024
513 |
514 | #: The content type to use for responses that do not explicitly define a
515 | #: ``Content-Type`` header.
516 | default_content_type = 'text/plain'
517 |
518 | #: Special response used to signal that a response does not need to be
519 | #: written to the client. Used to exit WebSocket connections cleanly.
520 | already_handled = None
521 |
522 | def __init__(self, body='', status_code=200, headers=None, reason=None):
523 | if body is None and status_code == 200:
524 | body = ''
525 | status_code = 204
526 | self.status_code = status_code
527 | self.headers = NoCaseDict(headers or {})
528 | self.reason = reason
529 | if isinstance(body, (dict, list)):
530 | self.body = json.dumps(body).encode()
531 | self.headers['Content-Type'] = 'application/json; charset=UTF-8'
532 | elif isinstance(body, str):
533 | self.body = body.encode()
534 | else:
535 | # this applies to bytes, file-like objects or generators
536 | self.body = body
537 |
538 | def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
539 | max_age=None, secure=False, http_only=False):
540 | """Add a cookie to the response.
541 |
542 | :param cookie: The cookie's name.
543 | :param value: The cookie's value.
544 | :param path: The cookie's path.
545 | :param domain: The cookie's domain.
546 | :param expires: The cookie expiration time, as a ``datetime`` object
547 | or a correctly formatted string.
548 | :param max_age: The cookie's ``Max-Age`` value.
549 | :param secure: The cookie's ``secure`` flag.
550 | :param http_only: The cookie's ``HttpOnly`` flag.
551 | """
552 | http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
553 | if path:
554 | http_cookie += '; Path=' + path
555 | if domain:
556 | http_cookie += '; Domain=' + domain
557 | if expires:
558 | if isinstance(expires, str):
559 | http_cookie += '; Expires=' + expires
560 | else:
561 | http_cookie += '; Expires=' + expires.strftime(
562 | '%a, %d %b %Y %H:%M:%S GMT')
563 | if max_age:
564 | http_cookie += '; Max-Age=' + str(max_age)
565 | if secure:
566 | http_cookie += '; Secure'
567 | if http_only:
568 | http_cookie += '; HttpOnly'
569 | if 'Set-Cookie' in self.headers:
570 | self.headers['Set-Cookie'].append(http_cookie)
571 | else:
572 | self.headers['Set-Cookie'] = [http_cookie]
573 |
574 | def complete(self):
575 | if isinstance(self.body, bytes) and \
576 | 'Content-Length' not in self.headers:
577 | self.headers['Content-Length'] = str(len(self.body))
578 | if 'Content-Type' not in self.headers:
579 | self.headers['Content-Type'] = self.default_content_type
580 | if 'charset=' not in self.headers['Content-Type']:
581 | self.headers['Content-Type'] += '; charset=UTF-8'
582 |
583 | def write(self, stream):
584 | self.complete()
585 |
586 | # status code
587 | reason = self.reason if self.reason is not None else \
588 | ('OK' if self.status_code == 200 else 'N/A')
589 | stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
590 | status_code=self.status_code, reason=reason).encode())
591 |
592 | # headers
593 | for header, value in self.headers.items():
594 | values = value if isinstance(value, list) else [value]
595 | for value in values:
596 | stream.write('{header}: {value}\r\n'.format(
597 | header=header, value=value).encode())
598 | stream.write(b'\r\n')
599 |
600 | # body
601 | can_flush = hasattr(stream, 'flush')
602 | try:
603 | for body in self.body_iter():
604 | if isinstance(body, str): # pragma: no cover
605 | body = body.encode()
606 | stream.write(body)
607 | if can_flush: # pragma: no cover
608 | stream.flush()
609 | except OSError as exc: # pragma: no cover
610 | if exc.errno in MUTED_SOCKET_ERRORS:
611 | pass
612 | else:
613 | raise
614 |
615 | def body_iter(self):
616 | if self.body:
617 | if hasattr(self.body, 'read'):
618 | while True:
619 | buf = self.body.read(self.send_file_buffer_size)
620 | if len(buf):
621 | yield buf
622 | if len(buf) < self.send_file_buffer_size:
623 | break
624 | if hasattr(self.body, 'close'): # pragma: no cover
625 | self.body.close()
626 | elif hasattr(self.body, '__next__'):
627 | yield from self.body
628 | else:
629 | yield self.body
630 |
631 | @classmethod
632 | def redirect(cls, location, status_code=302):
633 | """Return a redirect response.
634 |
635 | :param location: The URL to redirect to.
636 | :param status_code: The 3xx status code to use for the redirect. The
637 | default is 302.
638 | """
639 | if '\x0d' in location or '\x0a' in location:
640 | raise ValueError('invalid redirect URL')
641 | return cls(status_code=status_code, headers={'Location': location})
642 |
643 | @classmethod
644 | def send_file(cls, filename, status_code=200, content_type=None):
645 | """Send file contents in a response.
646 |
647 | :param filename: The filename of the file.
648 | :param status_code: The 3xx status code to use for the redirect. The
649 | default is 302.
650 | :param content_type: The ``Content-Type`` header to use in the
651 | response. If omitted, it is generated
652 | automatically from the file extension.
653 |
654 | Security note: The filename is assumed to be trusted. Never pass
655 | filenames provided by the user without validating and sanitizing them
656 | first.
657 | """
658 | if content_type is None:
659 | ext = filename.split('.')[-1]
660 | if ext in Response.types_map:
661 | content_type = Response.types_map[ext]
662 | else:
663 | content_type = 'application/octet-stream'
664 | f = open(filename, 'rb')
665 | return cls(body=f, status_code=status_code,
666 | headers={'Content-Type': content_type})
667 |
668 |
669 | class URLPattern():
670 | def __init__(self, url_pattern):
671 | self.url_pattern = url_pattern
672 | self.pattern = ''
673 | self.args = []
674 | use_regex = False
675 | for segment in url_pattern.lstrip('/').split('/'):
676 | if segment and segment[0] == '<':
677 | if segment[-1] != '>':
678 | raise ValueError('invalid URL pattern')
679 | segment = segment[1:-1]
680 | if ':' in segment:
681 | type_, name = segment.rsplit(':', 1)
682 | else:
683 | type_ = 'string'
684 | name = segment
685 | if type_ == 'string':
686 | pattern = '[^/]+'
687 | elif type_ == 'int':
688 | pattern = '\\d+'
689 | elif type_ == 'path':
690 | pattern = '.+'
691 | elif type_.startswith('re:'):
692 | pattern = type_[3:]
693 | else:
694 | raise ValueError('invalid URL segment type')
695 | use_regex = True
696 | self.pattern += '/({pattern})'.format(pattern=pattern)
697 | self.args.append({'type': type_, 'name': name})
698 | else:
699 | self.pattern += '/{segment}'.format(segment=segment)
700 | if use_regex:
701 | self.pattern = re.compile('^' + self.pattern + '$')
702 |
703 | def match(self, path):
704 | if isinstance(self.pattern, str):
705 | if path != self.pattern:
706 | return
707 | return {}
708 | g = self.pattern.match(path)
709 | if not g:
710 | return
711 | args = {}
712 | i = 1
713 | for arg in self.args:
714 | value = g.group(i)
715 | if arg['type'] == 'int':
716 | value = int(value)
717 | args[arg['name']] = value
718 | i += 1
719 | return args
720 |
721 |
722 | class HTTPException(Exception):
723 | def __init__(self, status_code, reason=None):
724 | self.status_code = status_code
725 | self.reason = reason or str(status_code) + ' error'
726 |
727 | def __repr__(self): # pragma: no cover
728 | return 'HTTPException: {}'.format(self.status_code)
729 |
730 |
731 | class Microdot():
732 | """An HTTP application class.
733 |
734 | This class implements an HTTP application instance and is heavily
735 | influenced by the ``Flask`` class of the Flask framework. It is typically
736 | declared near the start of the main application script.
737 |
738 | Example::
739 |
740 | from microdot import Microdot
741 |
742 | app = Microdot()
743 | """
744 |
745 | def __init__(self):
746 | self.url_map = []
747 | self.before_request_handlers = []
748 | self.after_request_handlers = []
749 | self.error_handlers = {}
750 | self.shutdown_requested = False
751 | self.debug = False
752 | self.server = None
753 |
754 | def route(self, url_pattern, methods=None):
755 | """Decorator that is used to register a function as a request handler
756 | for a given URL.
757 |
758 | :param url_pattern: The URL pattern that will be compared against
759 | incoming requests.
760 | :param methods: The list of HTTP methods to be handled by the
761 | decorated function. If omitted, only ``GET`` requests
762 | are handled.
763 |
764 | The URL pattern can be a static path (for example, ``/users`` or
765 | ``/api/invoices/search``) or a path with dynamic components enclosed
766 | in ``<`` and ``>`` (for example, ``/users/`` or
767 | ``/invoices//products``). Dynamic path components can also
768 | include a type prefix, separated from the name with a colon (for
769 | example, ``/users/``). The type can be ``string`` (the
770 | default), ``int``, ``path`` or ``re:[regular-expression]``.
771 |
772 | The first argument of the decorated function must be
773 | the request object. Any path arguments that are specified in the URL
774 | pattern are passed as keyword arguments. The return value of the
775 | function must be a :class:`Response` instance, or the arguments to
776 | be passed to this class.
777 |
778 | Example::
779 |
780 | @app.route('/')
781 | def index(request):
782 | return 'Hello, world!'
783 | """
784 | def decorated(f):
785 | self.url_map.append(
786 | (methods or ['GET'], URLPattern(url_pattern), f))
787 | return f
788 | return decorated
789 |
790 | def get(self, url_pattern):
791 | """Decorator that is used to register a function as a ``GET`` request
792 | handler for a given URL.
793 |
794 | :param url_pattern: The URL pattern that will be compared against
795 | incoming requests.
796 |
797 | This decorator can be used as an alias to the ``route`` decorator with
798 | ``methods=['GET']``.
799 |
800 | Example::
801 |
802 | @app.get('/users/')
803 | def get_user(request, id):
804 | # ...
805 | """
806 | return self.route(url_pattern, methods=['GET'])
807 |
808 | def post(self, url_pattern):
809 | """Decorator that is used to register a function as a ``POST`` request
810 | handler for a given URL.
811 |
812 | :param url_pattern: The URL pattern that will be compared against
813 | incoming requests.
814 |
815 | This decorator can be used as an alias to the``route`` decorator with
816 | ``methods=['POST']``.
817 |
818 | Example::
819 |
820 | @app.post('/users')
821 | def create_user(request):
822 | # ...
823 | """
824 | return self.route(url_pattern, methods=['POST'])
825 |
826 | def put(self, url_pattern):
827 | """Decorator that is used to register a function as a ``PUT`` request
828 | handler for a given URL.
829 |
830 | :param url_pattern: The URL pattern that will be compared against
831 | incoming requests.
832 |
833 | This decorator can be used as an alias to the ``route`` decorator with
834 | ``methods=['PUT']``.
835 |
836 | Example::
837 |
838 | @app.put('/users/')
839 | def edit_user(request, id):
840 | # ...
841 | """
842 | return self.route(url_pattern, methods=['PUT'])
843 |
844 | def patch(self, url_pattern):
845 | """Decorator that is used to register a function as a ``PATCH`` request
846 | handler for a given URL.
847 |
848 | :param url_pattern: The URL pattern that will be compared against
849 | incoming requests.
850 |
851 | This decorator can be used as an alias to the ``route`` decorator with
852 | ``methods=['PATCH']``.
853 |
854 | Example::
855 |
856 | @app.patch('/users/')
857 | def edit_user(request, id):
858 | # ...
859 | """
860 | return self.route(url_pattern, methods=['PATCH'])
861 |
862 | def delete(self, url_pattern):
863 | """Decorator that is used to register a function as a ``DELETE``
864 | request handler for a given URL.
865 |
866 | :param url_pattern: The URL pattern that will be compared against
867 | incoming requests.
868 |
869 | This decorator can be used as an alias to the ``route`` decorator with
870 | ``methods=['DELETE']``.
871 |
872 | Example::
873 |
874 | @app.delete('/users/')
875 | def delete_user(request, id):
876 | # ...
877 | """
878 | return self.route(url_pattern, methods=['DELETE'])
879 |
880 | def before_request(self, f):
881 | """Decorator to register a function to run before each request is
882 | handled. The decorated function must take a single argument, the
883 | request object.
884 |
885 | Example::
886 |
887 | @app.before_request
888 | def func(request):
889 | # ...
890 | """
891 | self.before_request_handlers.append(f)
892 | return f
893 |
894 | def after_request(self, f):
895 | """Decorator to register a function to run after each request is
896 | handled. The decorated function must take two arguments, the request
897 | and response objects. The return value of the function must be an
898 | updated response object.
899 |
900 | Example::
901 |
902 | @app.after_request
903 | def func(request, response):
904 | # ...
905 | return response
906 | """
907 | self.after_request_handlers.append(f)
908 | return f
909 |
910 | def errorhandler(self, status_code_or_exception_class):
911 | """Decorator to register a function as an error handler. Error handler
912 | functions for numeric HTTP status codes must accept a single argument,
913 | the request object. Error handler functions for Python exceptions
914 | must accept two arguments, the request object and the exception
915 | object.
916 |
917 | :param status_code_or_exception_class: The numeric HTTP status code or
918 | Python exception class to
919 | handle.
920 |
921 | Examples::
922 |
923 | @app.errorhandler(404)
924 | def not_found(request):
925 | return 'Not found'
926 |
927 | @app.errorhandler(RuntimeError)
928 | def runtime_error(request, exception):
929 | return 'Runtime error'
930 | """
931 | def decorated(f):
932 | self.error_handlers[status_code_or_exception_class] = f
933 | return f
934 | return decorated
935 |
936 | def mount(self, subapp, url_prefix=''):
937 | """Mount a sub-application, optionally under the given URL prefix.
938 |
939 | :param subapp: The sub-application to mount.
940 | :param url_prefix: The URL prefix to mount the application under.
941 | """
942 | for methods, pattern, handler in subapp.url_map:
943 | self.url_map.append(
944 | (methods, URLPattern(url_prefix + pattern.url_pattern),
945 | handler))
946 | for handler in subapp.before_request_handlers:
947 | self.before_request_handlers.append(handler)
948 | for handler in subapp.after_request_handlers:
949 | self.after_request_handlers.append(handler)
950 | for status_code, handler in subapp.error_handlers.items():
951 | self.error_handlers[status_code] = handler
952 |
953 | @staticmethod
954 | def abort(status_code, reason=None):
955 | """Abort the current request and return an error response with the
956 | given status code.
957 |
958 | :param status_code: The numeric status code of the response.
959 | :param reason: The reason for the response, which is included in the
960 | response body.
961 |
962 | Example::
963 |
964 | from microdot import abort
965 |
966 | @app.route('/users/')
967 | def get_user(id):
968 | user = get_user_by_id(id)
969 | if user is None:
970 | abort(404)
971 | return user.to_dict()
972 | """
973 | raise HTTPException(status_code, reason)
974 |
975 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
976 | """Start the web server. This function does not normally return, as
977 | the server enters an endless listening loop. The :func:`shutdown`
978 | function provides a method for terminating the server gracefully.
979 |
980 | :param host: The hostname or IP address of the network interface that
981 | will be listening for requests. A value of ``'0.0.0.0'``
982 | (the default) indicates that the server should listen for
983 | requests on all the available interfaces, and a value of
984 | ``127.0.0.1`` indicates that the server should listen
985 | for requests only on the internal networking interface of
986 | the host.
987 | :param port: The port number to listen for requests. The default is
988 | port 5000.
989 | :param debug: If ``True``, the server logs debugging information. The
990 | default is ``False``.
991 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should
992 | not use TLS. The default is ``None``.
993 |
994 | Example::
995 |
996 | from microdot import Microdot
997 |
998 | app = Microdot()
999 |
1000 | @app.route('/')
1001 | def index():
1002 | return 'Hello, world!'
1003 |
1004 | app.run(debug=True)
1005 | """
1006 | self.debug = debug
1007 | self.shutdown_requested = False
1008 |
1009 | self.server = socket.socket()
1010 | ai = socket.getaddrinfo(host, port)
1011 | addr = ai[0][-1]
1012 |
1013 | if self.debug: # pragma: no cover
1014 | print('Starting {mode} server on {host}:{port}...'.format(
1015 | mode=concurrency_mode, host=host, port=port))
1016 | self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1017 | self.server.bind(addr)
1018 | self.server.listen(5)
1019 |
1020 | if ssl:
1021 | self.server = ssl.wrap_socket(self.server, server_side=True)
1022 |
1023 | while not self.shutdown_requested:
1024 | try:
1025 | sock, addr = self.server.accept()
1026 | except OSError as exc: # pragma: no cover
1027 | if exc.errno == errno.ECONNABORTED:
1028 | break
1029 | else:
1030 | print_exception(exc)
1031 | except Exception as exc: # pragma: no cover
1032 | print_exception(exc)
1033 | else:
1034 | create_thread(self.handle_request, sock, addr)
1035 |
1036 | def shutdown(self):
1037 | """Request a server shutdown. The server will then exit its request
1038 | listening loop and the :func:`run` function will return. This function
1039 | can be safely called from a route handler, as it only schedules the
1040 | server to terminate as soon as the request completes.
1041 |
1042 | Example::
1043 |
1044 | @app.route('/shutdown')
1045 | def shutdown(request):
1046 | request.app.shutdown()
1047 | return 'The server is shutting down...'
1048 | """
1049 | self.shutdown_requested = True
1050 |
1051 | def find_route(self, req):
1052 | f = 404
1053 | for route_methods, route_pattern, route_handler in self.url_map:
1054 | req.url_args = route_pattern.match(req.path)
1055 | if req.url_args is not None:
1056 | if req.method in route_methods:
1057 | f = route_handler
1058 | break
1059 | else:
1060 | f = 405
1061 | return f
1062 |
1063 | def handle_request(self, sock, addr):
1064 | if not hasattr(sock, 'readline'): # pragma: no cover
1065 | stream = sock.makefile("rwb")
1066 | else:
1067 | stream = sock
1068 |
1069 | req = None
1070 | res = None
1071 | try:
1072 | req = Request.create(self, stream, addr, sock)
1073 | res = self.dispatch_request(req)
1074 | except Exception as exc: # pragma: no cover
1075 | print_exception(exc)
1076 | try:
1077 | if res and res != Response.already_handled: # pragma: no branch
1078 | res.write(stream)
1079 | stream.close()
1080 | except OSError as exc: # pragma: no cover
1081 | if exc.errno in MUTED_SOCKET_ERRORS:
1082 | pass
1083 | else:
1084 | print_exception(exc)
1085 | except Exception as exc: # pragma: no cover
1086 | print_exception(exc)
1087 | if stream != sock: # pragma: no cover
1088 | sock.close()
1089 | if self.shutdown_requested: # pragma: no cover
1090 | self.server.close()
1091 | if self.debug and req: # pragma: no cover
1092 | print('{method} {path} {status_code}'.format(
1093 | method=req.method, path=req.path,
1094 | status_code=res.status_code))
1095 |
1096 | def dispatch_request(self, req):
1097 | if req:
1098 | if req.content_length > req.max_content_length:
1099 | if 413 in self.error_handlers:
1100 | res = self.error_handlers[413](req)
1101 | else:
1102 | res = 'Payload too large', 413
1103 | else:
1104 | f = self.find_route(req)
1105 | try:
1106 | res = None
1107 | if callable(f):
1108 | for handler in self.before_request_handlers:
1109 | res = handler(req)
1110 | if res:
1111 | break
1112 | if res is None:
1113 | res = f(req, **req.url_args)
1114 | if isinstance(res, tuple):
1115 | body = res[0]
1116 | if isinstance(res[1], int):
1117 | status_code = res[1]
1118 | headers = res[2] if len(res) > 2 else {}
1119 | else:
1120 | status_code = 200
1121 | headers = res[1]
1122 | res = Response(body, status_code, headers)
1123 | elif not isinstance(res, Response):
1124 | res = Response(res)
1125 | for handler in self.after_request_handlers:
1126 | res = handler(req, res) or res
1127 | for handler in req.after_request_handlers:
1128 | res = handler(req, res) or res
1129 | elif f in self.error_handlers:
1130 | res = self.error_handlers[f](req)
1131 | else:
1132 | res = 'Not found', f
1133 | except HTTPException as exc:
1134 | if exc.status_code in self.error_handlers:
1135 | res = self.error_handlers[exc.status_code](req)
1136 | else:
1137 | res = exc.reason, exc.status_code
1138 | except Exception as exc:
1139 | print_exception(exc)
1140 | exc_class = None
1141 | res = None
1142 | if exc.__class__ in self.error_handlers:
1143 | exc_class = exc.__class__
1144 | else:
1145 | for c in mro(exc.__class__)[1:]:
1146 | if c in self.error_handlers:
1147 | exc_class = c
1148 | break
1149 | if exc_class:
1150 | try:
1151 | res = self.error_handlers[exc_class](req, exc)
1152 | except Exception as exc2: # pragma: no cover
1153 | print_exception(exc2)
1154 | if res is None:
1155 | if 500 in self.error_handlers:
1156 | res = self.error_handlers[500](req)
1157 | else:
1158 | res = 'Internal server error', 500
1159 | else:
1160 | if 400 in self.error_handlers:
1161 | res = self.error_handlers[400](req)
1162 | else:
1163 | res = 'Bad request', 400
1164 |
1165 | if isinstance(res, tuple):
1166 | res = Response(*res)
1167 | elif not isinstance(res, Response):
1168 | res = Response(res)
1169 | return res
1170 |
1171 |
1172 | abort = Microdot.abort
1173 | Response.already_handled = Response()
1174 | redirect = Response.redirect
1175 | send_file = Response.send_file
1176 |
--------------------------------------------------------------------------------