justpy-org / justpy

An object oriented high-level Python Web Framework that requires no frontend programming
https://justpy.io
Apache License 2.0
1.22k stars 96 forks source link

followup issue routing parameter #502

Closed WolfgangFahl closed 2 years ago

WolfgangFahl commented 2 years ago

as an amendment to #481 the routing as intended by #478 is broken. examples/tutorial/routes/route3.py has been modified slightly for this:

# Justpy Tutorial demo  from docs/tutorial/routes.md
import justpy as jp
def greeting_function(request):
    wp = jp.WebPage()
    name=f"""{request.path_params["name"]}"""
    wp.add(jp.P(text=f'Hello there, {name}!', classes='text-5xl m-2'))
    return wp
jp.Route('/hello/{name}', greeting_function)

# initialize the demo
from  examples.basedemo import Demo
Demo ("route3",None)
scripts/start examples/tutorial/routes/route3.py

http://localhost:8000/hello/budy

will lead to KeyError: 'name'

name=f"""{request.path_params["name"]}"""
WolfgangFahl commented 2 years ago

see https://github.com/encode/starlette/issues/1196

WolfgangFahl commented 2 years ago

This would be a way to move the homepage to the end - but it would only work for justpy special routing

def getRoutesByPriority(self):
        """
        get the routes by priority
        """
        routes=self.router.routes
        routes_by_priority=[]
        homepage=None
        for route in routes:
            if isinstance(route,Route):
                if route.name and route.name =="Homepage":
                    homepage=route
                else:
                    routes_by_priority.append(route)
        if homepage:
            routes_by_priority.append(homepage)
        return routes_by_priority
WolfgangFahl commented 2 years ago

see also https://github.com/tiangolo/fastapi/issues/317

WolfgangFahl commented 2 years ago

Original HomePage code:

@app.route("/{path:path}")
class Homepage(HTTPEndpoint):

    async def get(self, request):
        # Handle web requests
        session_cookie = request.cookies.get(SESSION_COOKIE_NAME)
        if SESSIONS:
            new_cookie = False
            if session_cookie:
                try:
                    session_id = cookie_signer.unsign(session_cookie).decode("utf-8")
                except:
                    return PlainTextResponse('Bad Session')
                request.state.session_id = session_id
                request.session_id = session_id
            else:
                # Create new session_id
                request.state.session_id = str(uuid.uuid4().hex)
                request.session_id = request.state.session_id
                new_cookie = True
                logging.debug(f'New session_id created: {request.session_id}')
        for route in Route.instances:
            func = route.matches(request['path'], request)
            if func:
                func_to_run = func
                break
        func_parameters = len(inspect.signature(func_to_run).parameters)
        assert func_parameters < 2, f"Function {func_to_run.__name__} cannot have more than one parameter"
        if inspect.iscoroutinefunction(func_to_run):
            if func_parameters == 1:
                load_page = await func_to_run(request)
            else:
                load_page = await func_to_run()
        else:
            if func_parameters == 1:
                load_page = func_to_run(request)
            else:
                load_page = func_to_run()
        if isinstance(load_page, Response):
            logging.debug('Returning raw starlette.responses.Response.')
            return load_page
        assert issubclass(type(load_page), WebPage), 'Function did not return a web page'
        assert len(load_page) > 0 or load_page.html, '\u001b[47;1m\033[93mWeb page is empty, add components\033[0m'
        page_options = {'reload_interval': load_page.reload_interval, 'body_style': load_page.body_style,
                        'body_classes': load_page.body_classes, 'css': load_page.css, 'head_html': load_page.head_html, 'body_html': load_page.body_html,
                        'display_url': load_page.display_url, 'dark': load_page.dark, 'title': load_page.title, 'redirect': load_page.redirect,
                        'highcharts_theme': load_page.highcharts_theme, 'debug': load_page.debug, 'events': load_page.events,
                        'favicon': load_page.favicon if load_page.favicon else FAVICON}
        if load_page.use_cache:
            page_dict = load_page.cache
        else:
            page_dict = load_page.build_list()
        template_options['tailwind'] = load_page.tailwind
        context = {'request': request, 'page_id': load_page.page_id, 'justpy_dict': json.dumps(page_dict, default=str),
                   'use_websockets': json.dumps(WebPage.use_websockets), 'options': template_options, 'page_options': page_options,
                   'html': load_page.html}
        response = templates.TemplateResponse(load_page.template_file, context)
        if SESSIONS and new_cookie:
            cookie_value = cookie_signer.sign(request.state.session_id)
            cookie_value = cookie_value.decode("utf-8")
            response.set_cookie(SESSION_COOKIE_NAME, cookie_value, max_age=COOKIE_MAX_AGE, httponly=True)
            for k, v in load_page.cookies.items():
                response.set_cookie(k, v, max_age=COOKIE_MAX_AGE, httponly=True)
        if LATENCY:
            await asyncio.sleep(LATENCY/1000)
        return response
WolfgangFahl commented 2 years ago

a wrap decorator might solve the issue - we tested this with

"""
Created on 2022-09-13

@author: wf
"""
import justpy as jp
from starlette.requests import Request
from tests.base_client_test import BaseClienttest
import time
# https://fastapi.tiangolo.com/tutorial/middleware/
@jp.app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    func=request.app.get_func_for_request(request)
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

@jp.app.route("/webpage", name="webpage")
@jp.app.wrap
def hello_world(_request):
    wp = jp.WebPage()
    _d = jp.Div(text='I am justpy webpage',a=wp)
    return wp

class TestMiddleWare(BaseClienttest):
    """
    test FASTAPI middleware 
    """

    def setUp(self, debug=False, profile=True):
        BaseClienttest.setUp(self, debug=debug, profile=profile)
        self.app.prioritize_routes()

    def test_middleware(self):
        """
        test the middleware 
        """
        response=self.checkResponse("/webpage",debug=True)
        pass   

and had to move the def get_response_for_load_page(self,request,load_page) to JustpyApp for a start.

def wrap(self,fn):
        """
        decorator
        """
        def funcResponse(request,*args,**kwargs):
            wp=fn(request)
            response=self.get_response_for_load_page(request, wp)
            return response
        pass

        return funcResponse
freemant2000 commented 2 years ago

Perhaps I am missing something. I don't understand why the code below is needed. Why not just add the route for the homepage only when the justpy() function is called? As it is the catch all page with a most generic path, it makes sense to add it last, right?

super().add_route(path,route,methods,name,include_in_schema)
        if name: 
            if name=="justpy_homepage":
                routes= self.router.routes.copy()
                last=routes[len(routes)-1]
                routes.remove(last)
                routes.insert(0,last)
                self.router.routes=routes
                pass
            else:
                JpRoute(path=path,endpoint=route,name=name)
        pass