jczic / MicroWebSrv

A micro HTTP Web server that supports WebSockets, html/python language templating and routing handlers, for MicroPython (used on Pycom modules & ESP32)
https://github.com/jczic/MicroWebSrv
MIT License
641 stars 116 forks source link

Variable route #16

Closed JK-de closed 6 years ago

JK-de commented 6 years ago

ADAPTED TO NEW IMPLEMENTATION

Using a '<...>' in the given route like @MicroWebSrv.route('/edit/<index>') is interpreted as variable part of the route which is passed as 3rd parameter to the handler function as args.index.

With the handler definition:

@MicroWebSrv.route('/edit/<index>')
def _httpHandlerEditWithArgs(httpClient, httpResponse, args) :

the following calls will result in:

<IP>/edit/123               ->   args['index']=123
<IP>/edit/a                 ->   args['index']='a'
<IP>/edit/Lorem.Ipsum       ->   args['index']='Lorem.Ipsum'
<IP>/edit/Lorem/Ipsum       ->   404

This can be used in templated html like (simplyfied):

<h1>Choose an item to edit:</h1>
{{ for item in my_list }}
  <a href="/edit/{{ item.id }}">{{ item.name }}</a>
  <br/>
{{ end }}
jczic commented 6 years ago

Okay, it's cool too :) I'll see that just when I have time ok ?

2018-02-14 18:47 GMT+01:00 Jochen Krapf notifications@github.com:

Using a '#' in the given route like @MicroWebSrv.route('/edit/#') is interpreted as variable part of the route which is passed as 3rd parameter to the handler function.

With the handler definition:

@MicroWebSrv.route('/edit/#') def _httpHandlerEditByID(httpClient, httpResponse, id='default') :

the following calls will result in:

/edit/123 -> id='123' /edit/a -> id='a' /edit/Lorem.Ipsum -> id='Lorem.Ipsum' This can be used in templated html like (simplyfied):

Choose an item to edit:

{{ for id, name in my_list }} {{ name }}
{{ end }} ------------------------------ You can view, comment on, or merge this pull request online at: https://github.com/jczic/MicroWebSrv/pull/16 Commit Summary - Added variable route File Changes - *M* main.py (27) - *M* microWebSrv.py (26) Patch Links: - https://github.com/jczic/MicroWebSrv/pull/16.patch - https://github.com/jczic/MicroWebSrv/pull/16.diff — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub , or mute the thread .

--

Sincèrement,

JK-de commented 6 years ago

No rush! You can test it together with the next pull request. ;-)

jczic commented 6 years ago

Just, do you think this is not better if the routes are like this :

/users/{id}?param1={param1}&param2={param2}

or :

/users/{userID}/addresses/{addrID}

and the handler could be return a class with :

    def _httpHandlerEditVariable(httpClient, httpResponse, routeArgs) :
        # routeArgs is a class, always existing with zero or more members
        if routeArgs.userID :
            ...
            if routeArgs.addrID :
            ...

No ? :)

jczic commented 6 years ago

I purpose a dedicated "routeArgs" class to create the routeArgs results with goods members. A constructor like routeArgs(self, originalPath, routingPath) will be cool :) Also, the integers could be automatically converted from strings.

JK-de commented 6 years ago

Hello @jczic,

?param1={param1}&param2={param2}

The params after '?' are handled by httpClient.ReadRequestPostedFormData()

/users/{userID}/addresses/{addrID}

The syntax of flask and django is /users/<userID>/addresses/<addrID>. I suggest to use '<' instead of '{' as in the big brothers

I purpose a dedicated "routeArgs" class...

I think a dict fits better...

Choosing the '#' was to keep it simple and reduce memory and code usage. And I thought that 1 param fits most application.

The main problem is not to extract the params from originalPath - the problem is to compare the list of all routes to the originalPath.

Possible implementation:

I will check in the next days if micropython re supports all features of groups and param handling.

JK-de commented 6 years ago

I see that micropython supports only numeric groups! So the route have to be converted in /users/(\w+)/addresses/(\w+) and the assignement to the names (userID, addrID) have to be done by hand...

jczic commented 6 years ago

I've a begining of solution for micropython with regex and no usage of findall or many groups ;) But yes, { } can be replaced by < > A class is cool I think, to directly use routeArgs.userID (in integer) You can check my works on this (do you understand all that ?) :


### WHEN A ROUTE IS ADDED ###

regRoute  = re.compile('{(\w*)}')
route     = '/users/{uID}/addresses/{addrID}/test/{anotherID}'
route     = regRoute.split(route)
# >> ['/users/', 'uID', '/addresses/', 'addrID', '/test/', 'anotherID', '']
regPath = ''
arg     = False
for s in route :
    regPath += '(\w*)' if arg else s
    arg = not arg
regPath += '$'
# >> '/users/(\w*)/addresses/(\w*)/test/(\w*)$'
regPath = re.compile(regPath)

### WHEN A REQUEST OCCURS ###

path = '/users/hello/addresses/123/test/456'

if regPath.match(path) :
    # >> Route is really OK
    splitted = regPath.split(path)
    # >> ['', 'hello', '123', '456', '']
    class cls :
        pass
    routeArgs = cls()
    if len(splitted) > 2 :
        idx       = 0
        arg       = False
        for s in route :
            if arg :
                idx += 1
                val  = splitted[idx]
                try :
                    val = int(val)
                except :
                    pass
                setattr(routeArgs, s, val)
            arg = not arg
        # >> routeArgs.uID       >> 'hello'
        # >> routeArgs.addrID    >> 123
        # >> routeArgs.anotherID >> 456
    # Call handler with routeArgs
jczic commented 6 years ago

But than can be better (between route added and same route checking, to just keep args and no all splitted elements.....)

jczic commented 6 years ago

New code, better and secured : You can test it directly :)

import re

REGEX_TO_SPLIT_ROUTE = re.compile('<(\w*)>')

### WHEN A ROUTE IS ADDED ###

route = '/users/<uID>/addresses/<addrID>/test/<anotherID>'

route = REGEX_TO_SPLIT_ROUTE.split(route)
# -> ['/users/', 'uID', '/addresses/', 'addrID', '/test/', 'anotherID', '']
routeArgsNames = []
routeRegex     = ''
arg            = False
for s in route :
    if arg :
        routeArgsNames.append(s)
    routeRegex += '(\w*)' if arg else s
    arg = not arg
routeRegex += '$'
# -> '/users/(\w*)/addresses/(\w*)/test/(\w*)$'
routeRegex = re.compile(routeRegex)

### WHEN A REQUEST OCCURS ###

path = '/users/hello/addresses/123/test/456'

if routeRegex.match(path) :
    # -> Route is really OK
    splitted = routeRegex.split(path)
    # -> ['', 'hello', '123', '456', '']
    if len(splitted) > 2 :
        class cls :
            pass
        routeArgs = cls()
        x = 0
        for argName in routeArgsNames :
            x += 1
            try :
                val = None
                val = splitted[x]
                val = int(val)
            except :
                pass
            setattr(routeArgs, argName, val)
        # -> routeArgs.uID       >> 'hello'
        # -> routeArgs.addrID    >> 123
        # -> routeArgs.anotherID >> 456
    else :
        routeArgs = None
    # Call handler with routeArgs here
    # Test :
    if routeArgs :
        print('routeArgs.uID       >> %s' % routeArgs.uID)
        print('routeArgs.addrID    >> %d' % routeArgs.addrID)
        print('routeArgs.anotherID >> %d' % routeArgs.anotherID)
    else :
        print('No routeArgs')
JK-de commented 6 years ago

Hello @jczic,

your code works fine in CPython but MicroPython has a limited split! It does not support brackets in re-string:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NotImplementedError: Splitting with sub-captures

The following line should work:

REGEX_TO_SPLIT_ROUTE = re.compile('[<>]')

My suggestion:

### WHEN A ROUTE IS ADDED ###

route = '/users/<uID>/addresses/<addrID>/test/<anotherID>'

routeParts = route.split('/')
# -> ['', 'users', '<uID>', 'addresses', '<addrID>', 'test', '<anotherID>']
routeArgsNames = []
routeRegex     = ''
for s in routeParts :
    if s.startswith('<') and s.endswith('>') :
        routeArgsNames.append(s[1:-1])
        routeRegex += '/(\w*)'
    elif s :
        routeRegex += '/' + s
routeRegex += '$'
# -> '/users/(\w*)/addresses/(\w*)/test/(\w*)$'
routeRegex = re.compile(routeRegex)

P.S. You have interesting working hours :-)

jczic commented 6 years ago

Hmm and if you change :

REGEX_TO_SPLIT_ROUTE = re.compile('<(\w*)>')

by :

REGEX_TO_SPLIT_ROUTE = re.compile('\<(\w*)\>')

?

JK-de commented 6 years ago

The round brackets '(' ')' are the problem! They stands for a capture group.

Prease try my version - it works in MicroPython and CPython

jczic commented 6 years ago

Ok I see that, but the problem is the same for the other part :/

JK-de commented 6 years ago

The solution for the 2nd part is:

m = routeRegex.match(path)
...
for i in range(1, int(str(m)[11:-1])) :   # hack because MicroPython has no __len__ in match
    arg = m.group(i)
    ...

Note: There is no function groups() in MicroPython