From 1fffc9c7c20c5bc786d4892ac1f16dd9c4772d3e Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 13 Aug 2024 09:29:27 +0100 Subject: [PATCH] Tinyweb: Sync with upstream. Fixes #978. Includes the following changes: Adjust log.exc to log.exception https://github.com/belyalov/tinyweb/commit/7669f03cdcbb62a847e7d4917673be52ad2f7e79 Logging module dropped support for exc. These adjustments use the exception method instead. Co-authored-by: Stephen Jefferson force lowercase headers and force uppercase method https://github.com/belyalov/tinyweb/commit/b4393ac65a9e966982fcb6f42844f4363494be2c Co-authored-by: eyJhb add compatibility for micropython above 1.19.0 https://github.com/belyalov/tinyweb/commit/d067b98dfd1d2c45081dd86359557037b81536d7 * uasyncio is renamed to asyncio * directly use core from asyncio Co-authored-by: Fabian Clemenz --- .../examples/common/lib/tinyweb/server.mpy | Bin 6313 -> 6524 bytes .../examples/common/lib/tinyweb/server.py | 60 +++++++++++++----- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/micropython/examples/common/lib/tinyweb/server.mpy b/micropython/examples/common/lib/tinyweb/server.mpy index a84163476fb01a6554bfbd5b6a72246187b02def..bbd6efe744523df8b68bbc3726515e7b0ad05b41 100644 GIT binary patch delta 3940 zcmahLdrTYW{qAh@ejIm~i^J3S@Y~qn9M>dG7Gn;uArCHK9(m#9aKIY@1SCz=X1)(- z+AiA0W@)W9X`-s8J+x_ewjoWcZ3XgZR!yt6X)S6Kv`P85e@xS)Nt?9a*^p&xr-A(4 z@B4kf@ArOuKW_a;;Mnm3tO1?0UA&x-XQyYb&reKG`Qz6vT%MVX&-vx)%a>t4nYiem zPfT5ZA%4+67oWWvpY_dL$0{o(r^m+=Q{!00g}Lifa$;KJ|A`Z5G}B6ah?h|DYjS*M zJ~2I&G8tdXEE@@&S!gdCot?fiAIGTC+4$V_m03BS`kBdQtd8`LWMykwp#gF|j6C3s_`LZcmY2>Zs?9S>bIigyZO!h8+l#I1eD?|XMtZc@vl+lLu&qV{!Q*#x4282Onulmj#?gWHq61eI-h-*5y(Q}urSZ6*3Gs3gO%@fE-#QYkLLL@* z$#NDoTNg<%e?P_^lw|zcck!}=sEt>Gnrh}Hd11bYskkKumkO8I*Y@t*s(@Popaec}@=8!tiEf4)GaLzdK%oYvBQ4-^hO62Vdnj|7u?SEXa!=I|g`V3M zBO|;1J15C6^X8_?Ti@5Cy608uReGsxsc5NmC%Top(KX<#H>H))CP%=;dQuO|YLj|o z4=8Uo+3k8{MUC!M&3-EiL#~irs@a|kwInZH`%}>D1%LcVDLw9M?sRYz)2h;TmF5Lv z9c*B_1#Gmps)g9t)W|T?n$8Hsll4%uMh2d&??#C6=Z%K?TfFvedwm2Y+jw+P`Gr!_ z#Q9vDo}p>+gvvO$!hD(t#E8mxb5R{hi#;mi2bX}`fWuPk-F9masDB7J{8lNkyL1-T z1s^a04rH$em;WEP0uqf?Rj)q`Lj}-2Ai}&(6hxwdcf|_Vm_a)7p;esiRScj zaC`I}ySv+~@67rR$_#rmNFIZWe^UzG`b7hI{8?ip`5P18{-ct_kDUOaN6?qhQ4~b& z%ecaHxLgq~!X+$zm&@U|IBeD=9}a&Vr;vp?2@bqZA%5*9=X!9+$2a;?()n8CLw?k< zETm3XI+Jy|C~m5BJx6>hN*ShsGQ%dH)dCLn+eliYD$PY(h`aZ8^h%4zvYB-Ms?xSV z+z&;vnH&f%$k#lDTGuGl#1o=|NSkK1YY=!R=>l5;;tZT|2QraKcMkMGr_gpYYHhUl z@B>J++e|a*6L21XUt|1a3)A{GxGU!X*x!B%k%2t8BRoXt)cf=`lEI6gjv8@X{{T{>NgkUgK?R0{$=*T6_4P3Pb+$pxGTBwEDc_ zAoxyRE_B^mbAbHbTxy#%Cvzi!gmPb8T<&ulb29*XdJG1p4DI|E8&zBsK&J zS!5&yz1zbqHU(f37m&|ce^qtLZRp=$!Xk5^lttB&T(&fm0@8naJ2%C z{;~nEl!p1b)1j4*#j_ceKKDqB!bw`6yn23U=8J&LL}iseD~hnlT%G)KOD0$^L-9b8 zE&m|m>vTP)HGyr5gy6eF=Ew6k?#4e$> zL*jBK)@`?Uc@oY`LAUE-^Mu3QchWay5v;=vz3`j#`>n@pBmD_q?@ks(-ZF$=!Kn%; ztWByCCfi-gFu;tfjGV~|gh~$ygx;?*G{@C%iD$Bmo`G3N&O&km0-N2?XP`RYY^iH;5t1Kz|v2UvU%LqBzoX6EJ#p{z17HiLCCH=cHbqmsi{d z;58er$TAV@;(XrT9xGgMY>TJwMMw%+nJKP;X9Of$EiYQdNoUn z^bGi_hc@cGQjJX0b54~$&*LgRye0Ok^dK{<(sQ{ca-Kq6X?ljo`_BwQBl@QC6D1g$ zhcxL%;pM-lUSg`*nyrp5@(N7;Fbq=83SRb_NFnu8rVejN{hH}ZHtUZ8NJ9ObdP}x7Qi{egoKUs21v6}B;3+%vp^-s#SFc`0;gwiw zg{?`NbV_a|u2f9KfYt!S84G5-o0aj!iPxA~R1efT`Mlfi4A?Mc-iGU{+6WI$K2_F> zXKe;hg3t}RsB4~;zcs83_Migbe+(TTS$QdD5!#%*(`mQZPPyF^E{CVto=65yxSS$| ziWyX>rJ;`()2K)=D$!&V9_D~8&Y?H38SZd)K^}KnTRcJ5UO559W#hh6054ncC>+12GH9p0Zrq*cLk=n&+fd z#sy6Xs$((s-pjak4>IQ+gifryr4;%+oXVhrlw__uxDV)ntO%Z?)ewHw;_>OSYo1nH V{@qvELacA(4&XgC67oy(zW{BPgI@pu delta 3663 zcmai1Yj6|S72aLRwy=$TwR^pmZDGk;>n#gK$P3{q%j<_MOuT>$h7c9mucm1IJj zAGODK&wYLO+;hHj&h@{-KYh$JT1(l;n1g*Yge~ao)3eW?OwXL6+SjCD`qY^zZTG+F zVuJaAHr7s^OXr-%OAMJT{>;eG9gUM`PMw{bqS(nZQ?sYeo=HzpO;wQG%^GKoHNI~$ zS^TW#_e}2DbzPa)jFv5QD(Z4qevctuC-3zhQs4 z4w+%VET>Hem1r%iu=4QEuo;?J2v@)9v%^@C~_Dj8O~XMdp1Kw<3#3U)Pl6 zvPR?}lWdtxL~pRfTa3`>Ka!3VSmI-eKbNn!Wb`p*RT?TF%Y)^ZR479G%S<7ieR8I~ z?X(g|*#(cY#~1ensOvg#yGSn=CKLV}VX6q-4k9uU`X*qPA~ac*;>fC%l;n79xI~f_ ziPO?F+0tiv{8?p_pHe3i_19TWi_jZ!ERxQU6DCvB`gwD28#sx zaKOu!cl#VsB2Fo3rNQp@yBuDh$IaGar$|kdPZ6o7yM`%n&fIKSa5So%7_77;vUn+Oqkb%qUYQ{8?*7ejEuh(zg3`#1= z8^c{i0RabCV#oajkxmx{^Qtt$wiR%f2w{;PF&|*bmitnCV$F6(*f~UwXDjZmoDWUXBg}KN2?bo^TBx$Sth5W^^AvuH?I2F5SnH#to>J zkb?{ZYtW!-ahIJgr?clijaquZIzLewMuR;AZh^&Nm2<0HugF}78{r|5noO8O%<>Hl z4dZa3C^8SO$9goh>B03iU*S;21Q3ez=ickRnzP!f`$bAb@}p8f^Z5Xqks_ z#N+2ZfVQ3NP(D@G72r&58~#>ui?9-&3=@oT+Sy=T#T5OCBF zqi8p5{D)HK(>EH>=%bgB)2~mz^=HLQ@7@E4?j*ZNl=P9<0+rj^Y|V7xC%{s-Skk-n zGSKRF*&0iGKsVo*71M6Nx^1I^s+@G48Hw@mFrhgOfe2?3!Bw6C%_UX-jA9GKq zb&f`@>o$;$Bt#xt=*@K@Pq?$}r5h?|6PS-Bxrps-8+0lh7HNo72NIGh*PRS8H&=9h zaxWljrV_?12`{n48DS)aAeUxwYv4gI^1)p|5)6W|D%Bu=SqebxYirC{noMlJ4TQOZ zgJ`2jN9a2m4xz_z_$pYVOc&^1c_^Vx6;anA12{28UmmFp#{RCbqkB;Y1(Ca*ugrUWhJBl$0Jnu3c+w%KZH?KK zmQ`pEB!UA6Ck`#lDows__;Gd*46wwO3$vmul8p;*r0m||fLGo)=sc=%J5=s45CK|{ zlWA!1(}Go`$={YimJ3iltLazyP)S5TjX+Y8Hpa9qpGj9Iedh=?{bLHO^6hIUEH6O(oY1m>>S)eI|8`h})!v zLTbq68Vt;MjzxXmXL_dHzOg4lC+(u+plt~Lj&1a39J`$p`(}cPYG}mYHVj{+*=7iw z8!Uoi1?Aa>kE=LcEQyTD9S|AaPera<=vni{dum8I}U0L&2&}vLy6fc6)p+LdXM-CmfKdPC^9g;>_ z6F#+A;w0`gC^f=WU=k~FT9$MF#MaGTm3t~etK7j=DWP)X_>9WUmh;6^EEy_rrz7;f z!{bo#-Y|ZoL}PQ1E{92c;q%;ROFM5}?H@#Ep`=4xZV_x?=K#o^~e&2>2XbOE0kVaRGB|qo`E{RRNi8FbM-A&9xuPe zsuS#2CN8GFyATb$m;r@H$Z%dznuUJHo`JC2>)Dj>Q#S+D%5bd~p>B<=2b(1BO9K2qoc8M#aa< z8(Nn{0EC^Er2r)|B6If|u=whDnZJ uasyncio no named asyncio +# asyncio v3 is shipped with MicroPython 1.13, and contains some subtle # but breaking changes. See also https://github.com/peterhinch/micropython-async/blob/master/v3/README.md -IS_UASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,) +IS_ASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,) def urldecode_plus(s): """Decode urlencoded string (including '+' char). + Returns decoded string """ s = s.replace('+', ' ') @@ -42,6 +44,7 @@ def urldecode_plus(s): def parse_query_string(s): """Parse urlencoded string into dict. + Returns dict """ res = {} @@ -75,6 +78,7 @@ def __init__(self, _reader): async def read_request_line(self): """Read and parse first line (AKA HTTP Request Line). Function is generator. + Request line is something like: GET /something/script?param1=val1 HTTP/1.1 """ @@ -97,7 +101,9 @@ async def read_headers(self, save_headers=[]): """Read and parse HTTP headers until \r\n\r\n: Optional argument 'save_headers' controls which headers to save. This is done mostly to deal with memory constrains. + Function is generator. + HTTP headers could be like: Host: google.com Content-Type: blah @@ -111,12 +117,13 @@ async def read_headers(self, save_headers=[]): frags = line.split(b':', 1) if len(frags) != 2: raise HTTPException(400) - if frags[0] in save_headers: + if frags[0].lower() in save_headers: self.headers[frags[0]] = frags[1].strip() async def read_parse_form_data(self): """Read HTTP form data (payload), if any. Function is generator. + Returns: - dict of key / value pairs - None in case of no form data present @@ -163,6 +170,7 @@ async def _send_headers(self): - HTTP request line - HTTP headers following by \r\n. This function is generator. + P.S. Because of usually we have only a few HTTP headers (2-5) it doesn't make sense to send them separately - sometimes it could increase latency. @@ -181,8 +189,10 @@ async def _send_headers(self): async def error(self, code, msg=None): """Generate HTTP error response This function is generator. + Arguments: code - HTTP response code + Example: # Not enough permissions. Send HTTP 403 - Forbidden await resp.error(403) @@ -197,8 +207,10 @@ async def error(self, code, msg=None): async def redirect(self, location, msg=None): """Generate HTTP redirect response to 'location'. Basically it will generate HTTP 302 with 'Location' header + Arguments: location - URL to redirect to + Example: # Redirect to /something await resp.redirect('/something') @@ -213,9 +225,11 @@ async def redirect(self, location, msg=None): def add_header(self, key, value): """Add HTTP response header + Arguments: key - header name value - header value + Example: resp.add_header('Content-Encoding', 'gzip') """ @@ -232,6 +246,7 @@ def add_access_control_headers(self): async def start_html(self): """Start response with HTML content type. This function is generator. + Example: await resp.start_html() await resp.send('

Hello, world!

') @@ -242,6 +257,7 @@ async def start_html(self): async def send_file(self, filename, content_type=None, content_encoding=None, max_age=2592000, buf_size=128): """Send local file as HTTP response. This function is generator. + Arguments: filename - Name of file which exists in local filesystem Keyword arguments: @@ -249,10 +265,13 @@ async def send_file(self, filename, content_type=None, content_encoding=None, ma max_age - Cache control. How long browser can keep this file on disk. By default - 30 days Set to 0 - to disable caching. + Example 1: Default use case: await resp.send_file('images/cat.jpg') + Example 2: Disable caching: await resp.send_file('static/index.html', max_age=0) + Example 3: Override content type: await resp.send_file('static/file.bin', content_type='application/octet-stream') """ @@ -331,7 +350,7 @@ async def restful_resource_handler(req, resp, param=None): gc.collect() await resp.send('0\r\n\r\n') else: - if type(res) is tuple: + if type(res) == tuple: resp.code = res[1] res = res[0] elif res is None: @@ -457,22 +476,22 @@ async def _handler(self, reader, writer): try: await resp.error(500) except Exception as e: - log.exc(e, "") + log.exception(f"Failed to send 500 error after OSError. Original error: {e}") except HTTPException as e: try: await resp.error(e.code) except Exception as e: - log.exc(e) + log.exception(f"Failed to send error after HTTPException. Original error: {e}") except Exception as e: # Unhandled expection in user's method log.error(req.path.decode()) - log.exc(e, "") + log.exception(f"Unhandled exception in user's method. Original error: {e}") try: await resp.error(500) # Send exception info if desired if self.debug: sys.print_exception(e, resp.writer.s) - except Exception: + except Exception as e: pass finally: await writer.aclose() @@ -485,9 +504,11 @@ async def _handler(self, reader, writer): def add_route(self, url, f, **kwargs): """Add URL to function mapping. + Arguments: url - url to map function with f - function to map + Keyword arguments: methods - list of allowed methods. Defaults to ['GET', 'POST'] save_headers - contains list of HTTP headers to be saved. Case sensitive. Default - empty. @@ -507,8 +528,8 @@ def add_route(self, url, f, **kwargs): params.update(kwargs) params['allowed_access_control_methods'] = ', '.join(params['methods']) # Convert methods/headers to bytestring - params['methods'] = [x.encode() for x in params['methods']] - params['save_headers'] = [x.encode() for x in params['save_headers']] + params['methods'] = [x.encode().upper() for x in params['methods']] + params['save_headers'] = [x.encode().lower() for x in params['save_headers']] # If URL has a parameter if url.endswith('>'): idx = url.rfind('<') @@ -526,14 +547,18 @@ def add_route(self, url, f, **kwargs): def add_resource(self, cls, url, **kwargs): """Map resource (RestAPI) to URL + Arguments: cls - Resource class to map to url - url to map to class kwargs - User defined key args to pass to the handler. + Example: class myres(): def get(self, data): return {'hello': 'world'} + + app.add_resource(myres, '/api/myres') """ methods = [] @@ -556,6 +581,7 @@ def get(self, data): def catchall(self): """Decorator for catchall() + Example: @app.catchall() def catchall_handler(req, resp): @@ -572,6 +598,7 @@ def _route(f): def route(self, url, **kwargs): """Decorator for add_route() + Example: @app.route('/') def index(req, resp): @@ -585,10 +612,12 @@ def _route(f): def resource(self, url, method='GET', **kwargs): """Decorator for add_resource() method + Examples: @app.resource('/users') def users(data): return {'a': 1} + @app.resource('/messages/') async def index(data, topic_id): yield '{' @@ -617,8 +646,8 @@ async def _tcp_server(self, host, port, backlog): sock.listen(backlog) try: while True: - if IS_UASYNCIO_V3: - yield uasyncio.core._io_queue.queue_read(sock) + if IS_ASYNCIO_V3: + yield asyncio.core._io_queue.queue_read(sock) else: yield asyncio.IORead(sock) csock, caddr = sock.accept() @@ -645,6 +674,7 @@ async def _tcp_server(self, host, port, backlog): def run(self, host="127.0.0.1", port=8081, loop_forever=True): """Run Web Server. By default it runs forever. + Keyword arguments: host - host to listen on. By default - localhost (127.0.0.1) port - port to listen on. By default - 8081