Esempio n. 1
0
def setup_app():
    setup_logging()

    cookie_store = CookieStore(**settings.strict_get("sessions"))

    get_schema = OpenAPIHandler(
        Metadata(
            title="Chat",
            description="A simple chat app.",
            version="0.0.0",
        ), )

    get_docs = OpenAPIUIHandler()

    app = App(
        components=[
            AccountManagerComponent(),
            ChatHandlerFactoryComponent(),
            ChatroomListenerComponent(),
            ChatroomRegistryComponent(),
            CurrentAccountComponent(),
            PasswordHasherComponent(),
            RedisComponent(),
            SQLAlchemyEngineComponent(),
            SQLAlchemySessionComponent(),
            SessionComponent(cookie_store),
            SettingsComponent(settings),
            TemplatesComponent(path_to("templates")),
        ],
        middleware=[
            RequestIdMiddleware(),
            SessionMiddleware(cookie_store),
            ResponseRendererMiddleware(),
            WebsocketsMiddleware(),
            SQLAlchemyMiddleware(),
        ],
        routes=[
            Route("/_schema", get_schema),
            Route("/_docs", get_docs),
            Route("/", index),
            Route("/login", login),
            Route("/register", register),
            Include("/v1", [
                Include("/accounts", accounts.routes, namespace="accounts"),
                Include("/chat", chat.routes, namespace="chat"),
                Include("/sessions", sessions.routes, namespace="sessions"),
            ],
                    namespace="v1"),
        ],
    )

    decorated_app = WhiteNoise(app, **settings.strict_get("whitenoise"))
    return decorated_app, app
Esempio n. 2
0
def test_apps_session_cookies_expire():
    # Given that I have an app with a cookie store that immediately expires session cookies
    cookie_store = CookieStore(b"secret", cookie_ttl=0)

    def set_username(username: str, session: Session) -> None:
        session["username"] = username

    def get_username(session: Session) -> Optional[str]:
        return session.get("username")

    app = App(
        components=[
            SessionComponent(cookie_store),
        ],

        middleware=[
            SessionMiddleware(cookie_store),
            ResponseRendererMiddleware(),
        ],

        routes=[
            Route("/set-username/{username}", set_username),
            Route("/get-username", get_username),
        ],
    )

    # And a client for that app
    client = testing.TestClient(app)

    # When I make a request to a handler that stores session data
    response = client.get(app.reverse_uri("set_username", username="******"))

    # Then I should get back a successful response
    assert response.status_code == 200

    # And the response should contain my session cookie
    cookie = cookies.SimpleCookie()
    for data in response.headers.get_all("set-cookie"):
        cookie.load(data.replace("SameSite=Strict", ""))

    assert "__sess__" in cookie

    # When I make another request with that same cookie
    session_cookie = cookie.output(attrs=[], header="")
    response = client.get(app.reverse_uri("get_username"), headers={
        "cookie": session_cookie,
    })

    # Then I should get back nothing
    assert response.json() is None
Esempio n. 3
0
def test_middleware_token_validation_raises_error_on_token_exp(app_settings):
    def test_handler(jwt_identity: JWTIdentity):
        if jwt_identity is None:
            return "No user token present"
        return jwt_identity.id

    jwt = JWT(key="keepthissafe", alg="HS256")
    iat = dt.datetime.now() + dt.timedelta(seconds=-10)
    exp = iat + dt.timedelta(seconds=5)
    payload = {"sub": "1234567890", "name": "John Doe", "iat": iat, "exp": exp}
    token = jwt.encode(payload)

    routes = [Route("/auth-required", method="GET", handler=test_handler)]

    components = [
        SettingsComponent(app_settings),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/auth-required",
                          headers={"Authorization": f"Bearer {token}"})
    assert 401 == response.status_code
    content = response.json()
    assert "error_message" in content
    assert content.get("status") == 401
Esempio n. 4
0
def test_openapi_can_render_fields_with_different_request_and_response_names():
    # Given that I have a schema that has different names based on whether it's in the request or response
    @schema
    class A:
        x: int = field(request_name="X", response_name="Y")

    def index(a: A) -> A:
        pass

    # And an app
    app = App(routes=[Route("/", index)])

    # When I generate a document
    document = generate_openapi_document(
        app, Metadata("example", "an example", "0.0.0"), [])

    # Then the schema should mark that field as writeOnly
    response_schema = document["components"]["schemas"][
        "tests.openapi.test_openapi.A"]
    assert response_schema["properties"] == {
        "X": {
            "type": "integer",
            "format": "int64",
            "writeOnly": True,
        },
        "Y": {
            "type": "integer",
            "format": "int64",
            "readOnly": True,
        },
    }
Esempio n. 5
0
def test_openapi_can_render_request_only_fields():
    # Given that I have a schema that has request-only fields
    @schema
    class A:
        x: int = field(request_only=True)

    def index(a: A) -> A:
        pass

    # And an app
    app = App(routes=[Route("/", index)])

    # When I generate a document
    document = generate_openapi_document(
        app, Metadata("example", "an example", "0.0.0"), [])

    # Then the schema should mark that field as writeOnly
    response_schema = document["components"]["schemas"][
        "tests.openapi.test_openapi.A"]
    assert response_schema["properties"] == {
        "x": {
            "type": "integer",
            "format": "int64",
            "writeOnly": True,
        },
    }
Esempio n. 6
0
def test_middleware_white_listing(app_settings, testing_token):
    def test_handler(jwt_identity: JWTIdentity):
        if jwt_identity is None:
            return "No user token present"
        return jwt_identity.id

    routes = [Route("/whitelisted", method="GET", handler=test_handler)]

    components = [
        SettingsComponent({
            **app_settings, "JWT_AUTH_WHITELIST": ["test_handler"]
        }),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/whitelisted",
                          headers={"Authorization": f"Bearer {testing_token}"})

    unauthenticated = client.get("/whitelisted")
    assert 200 == response.status_code
    assert "1234567890" in response.data
    assert 200 == unauthenticated.status_code
    assert "No user token present" in unauthenticated.data
Esempio n. 7
0
def _create_molten_routes() -> Sequence[Route]:
    routes = []

    for endpoint in _ENDPOINTS:
        url = endpoint.method.__url__  # type: ignore # noqa: WPS609
        method = endpoint.method.__method__  # type: ignore # noqa: WPS609
        routes.append(Route(url, endpoint, method=method))

    return routes
Esempio n. 8
0
def test_router_can_match_nested_routes():
    # Given that I have a router with some nested routes
    router = Router([
        Include("/v1", [
            Include("/accounts", [
                Route("/", handler, name="get_accounts"),
                Route("/{account_id}", handler, name="get_account"),
            ]),
        ]),
    ])

    # When I match either of the routes
    # Then I should get back their Route objects and path params
    assert router.match("GET", "/v1/accounts/")
    assert router.match("GET", "/v1/accounts/1")

    # When I match a route that doesn't exist
    # Then I should get back None
    assert router.match("GET", "/v1") is None
Esempio n. 9
0
def test_empty_app_can_return_openapi_document():
    # Given that I have an empty app
    app = App(routes=[
        Route("/schema.json",
              OpenAPIHandler(
                  Metadata(
                      title="empty application",
                      description="an application that doesn't do anything",
                      version="0.1.0",
                      contact=Contact(name="Jim Gordon", ),
                  )),
              name="schema"),
    ])

    # When I visit its schema uri
    response = testing.TestClient(app).get("/schema.json")

    # Then I should get back a successful response
    assert response.status_code == 200
    assert response.json() == {
        "openapi": "3.0.1",
        "info": {
            "title": "empty application",
            "description": "an application that doesn't do anything",
            "version": "0.1.0",
            "contact": {
                "name": "Jim Gordon",
            },
        },
        "paths": {
            "/schema.json": {
                "get": {
                    "tags": [],
                    "operationId": "schema",
                    "summary": "Generates an OpenAPI v3 document.",
                    "description": "",
                    "deprecated": False,
                    "parameters": [],
                    "responses": {
                        "200": {
                            "description": "A successful response.",
                            "content": {},
                        },
                    },
                }
            }
        },
        "components": {
            "schemas": {},
            "securitySchemes": {},
        },
    }
Esempio n. 10
0
def test_identity_extract_jwt_from_cookie(app_settings):
    def test_auth(jwt: JWT):
        cookie_name = "molten_auth_cookie"
        cookie_value = jwt.encode({"sub": 123456, "name": "spiderman"})
        auth_response = Response(HTTP_200)
        auth_response.set_cookie(Cookie(cookie_name, cookie_value))
        return auth_response

    def test_cookie(jwt_identity: JWTIdentity):
        if jwt_identity is None:
            return "Didn't work"
        return f"Hello {jwt_identity.name} your sub id is {jwt_identity.sub}"

    routes = [
        Route("/auth", method="POST", handler=test_auth),
        Route("/cookie", method="GET", handler=test_cookie),
    ]

    components = [
        SettingsComponent({
            **app_settings,
            **{
                "JWT_AUTH_COOKIE": "molten_auth_cookie"
            }
        }),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    app = App(routes=routes, components=components)
    client = testing.TestClient(app)

    auth_response = client.post("/auth")
    cookie_value = auth_response.headers.get_all("set-cookie")[0]
    assert "molten_auth_cookie" in cookie_value
    cookie_response = client.get("/cookie", headers={"cookie": cookie_value})
    assert "123456" in cookie_response.data
    assert "spiderman" in cookie_response.data
Esempio n. 11
0
def setup_app():
    setup_logging()

    get_docs = OpenAPIUIHandler()
    get_schema = OpenAPIHandler(
        metadata=Metadata(
            title="Pets",
            description="",
            version="0.0.0",
        ),
    )

    app = App(
        components=[
            ManagerComponent(PetManager),
            SQLAlchemyEngineComponent(),
            SQLAlchemySessionComponent(),
            SettingsComponent(settings),
            TemplatesComponent(path_to("templates")),
        ],

        middleware=[
            RequestIdMiddleware(),
            ResponseRendererMiddleware(),
            SQLAlchemyMiddleware(),
        ],

        routes=[
            Route("/_docs", get_docs),
            Route("/_schema", get_schema),
            Route("/", index),
            Include("/v1/pets", pets.routes, namespace="pets"),
        ],
    )

    decorated_app = WhiteNoise(app, **settings.strict_get("whitenoise"))
    return decorated_app, app
Esempio n. 12
0
def test_claims_required_attaches_values(app_settings):
    @claims_required({"admin": True})
    def test_handler():
        return "Handler called"

    routes = [Route("/claims", method="GET", handler=test_handler)]

    components = [SettingsComponent(app_settings)]

    middleware = [ResponseRendererMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/claims")
    assert 200 == response.status_code
    assert "admin" in test_handler.claims
    assert test_handler.claims.get("admin") is True
    assert "Handler called" in response.data
Esempio n. 13
0
def test_middleware_raises_401_error(app_settings):
    def test_handler():
        return "Handler called"

    routes = [Route("/auth-required", method="GET", handler=test_handler)]

    components = [
        SettingsComponent(app_settings),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/auth-required")
    assert 401 == response.status_code
Esempio n. 14
0
def test_openapi_can_render_documents_with_method_handlers():
    # Given that I have a resource class
    @schema
    class User:
        username: str

    class Users:
        def get_users(self) -> User:
            pass

    # And an app that uses that an instance of that resource
    users = Users()
    app = App(routes=[Route("/users", users.get_users)])

    # When I generate a document
    document = generate_openapi_document(
        app, Metadata("example", "an example", "0.0.0"), [])

    # Then I should get back a valid document
    assert document
Esempio n. 15
0
def test_openapi_can_render_lists_of_x(fields, expected):
    # Given that I have a schema that has a list of something in it
    A = type("A", (object, ), fields)
    A.__annotations__ = fields
    A = schema(A)

    def index() -> A:
        pass

    # And an app
    app = App(routes=[Route("/", index)])

    # When I generate a document
    document = generate_openapi_document(
        app, Metadata("example", "an example", "0.0.0"), [])

    # Then the return schema should have an array of that thing
    response_schema = document["components"]["schemas"][
        "tests.openapi.test_openapi.A"]
    assert response_schema["properties"] == expected
Esempio n. 16
0
def test_JWT_claims_options_raises_error(app_settings, testing_token):
    def test_handler(jwt_identity: JWTIdentity):
        if jwt_identity is None:
            return "No user token present"
        return jwt_identity.id

    jwt = JWT(key=app_settings.get("JWT_SECRET_KEY"), alg="HS256")
    mod_token = jwt.encode({
        **jwt.decode(testing_token),
        **{
            "iss": "https://molten.com"
        }
    })

    routes = [Route("/claim_options", method="GET", handler=test_handler)]

    components = [
        SettingsComponent({
            **app_settings,
            "JWT_CLAIMS_OPTIONS": {
                "iss": {
                    "essential": True,
                    "values": ["https://example.com", "https://example.org"],
                }
            },
        }),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    missing_claim_response = client.get(
        "/claim_options", headers={"Authorization": f"Bearer {testing_token}"})
    wrong_claim_value_response = client.get(
        "/claim_options", headers={"Authorization": f"Bearer {mod_token}"})
    assert 401 == missing_claim_response.status_code
    assert 401 == wrong_claim_value_response.status_code
Esempio n. 17
0
def test_middleware_anonymous_user_support(app_settings):
    @allow_anonymous
    def test_handler():
        return "Handler called"

    routes = [Route("/auth-maybe", method="GET", handler=test_handler)]

    components = [
        SettingsComponent(app_settings),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/auth-maybe")
    assert 200 == response.status_code
    assert "Handler called" in response.data
Esempio n. 18
0
def test_claims_required_raises_error(app_settings, testing_token):
    @claims_required({"admin": True})
    def test_handler():
        return "Handler called"

    routes = [Route("/claims", method="GET", handler=test_handler)]

    components = [
        SettingsComponent(app_settings),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/claims",
                          headers={"Authorization": f"Bearer {testing_token}"})
    assert 403 == response.status_code
Esempio n. 19
0
def create_app(middleware=None, components=None, settings=None):
    if settings is None:
        settings = Settings({
            'database_engine_dsn': os.environ['SQLALCHEMY_URI'],
            'identity_server': os.environ['IDENTITY_SERVER'],
            'secret_key': os.environ['SECRET_KEY'],
        })

    if middleware is None:
        middleware = [
            ResponseRendererMiddleware(),
            SQLAlchemyMiddleware(),
            auth_middleware,
        ]

    if components is None:
        components = [
            SettingsComponent(settings),
            SQLAlchemyEngineComponent(),
            SQLAlchemySessionComponent(),
            RequestSessionComponent(),
            AuthProviderComponent(),
            ManagerComponent(ChoreInstanceManager),
            ManagerComponent(ChoreDefinitionManager),
            UserProviderComponent(),
        ]

    app = App(
        routes=[
            Route('/login', login, method='POST', name='login'),
            Include(
                '/api',
                routes,
                namespace='api',
            )
        ],
        middleware=middleware,
        components=components,
    )
    return app
Esempio n. 20
0
def test_ws_middleware_validates_origin():
    # Given that I have an app instance whose ws middleware validates the incoming origin
    app = App(
        middleware=[
            ResponseRendererMiddleware(),
            WebsocketsMiddleware(re.compile("example.com")),
        ],
        routes=[Route("/echo", echo)],
    )

    # And a ws client for that app
    client = WebsocketsTestClient(app)

    # When I try to connect to its echo endopoint with an invalid origin
    # Then an error should occur
    with pytest.raises(ValueError):
        client.connect("/echo")

    # When I try to connect to its echo endopoint with a valid origin
    with client.connect("/echo", headers={"origin": "example.com"}) as sock:
        # Then my connection should succeed
        assert sock
Esempio n. 21
0
def test_middleware_token_validation_passes(app_settings, testing_token):
    def test_handler(jwt_identity: JWTIdentity):
        if jwt_identity is None:
            return "No user token present"
        return jwt_identity.id

    routes = [Route("/auth-required", method="GET", handler=test_handler)]

    components = [
        SettingsComponent(app_settings),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    middleware = [ResponseRendererMiddleware(), JWTAuthMiddleware()]

    app = App(routes=routes, components=components, middleware=middleware)
    client = testing.TestClient(app)

    response = client.get("/auth-required",
                          headers={"Authorization": f"Bearer {testing_token}"})
    assert 200 == response.status_code
    assert "1234567890" in response.data
Esempio n. 22
0
def test_app_can_render_mail_templates():
    mail = Mail(
        user="******",
        password="******",
        port=587,
        use_tls=True,
        suppress_send=True,
    )
    mail.send = MagicMock(return_value=None)

    def template_handler(mail_templates: MailTemplates) -> Response:
        mail.send_message(
            subject="Test email",
            html=mail_templates.render("test_template.html", name="Molten"),
            recipients=["*****@*****.**"],
        )
        return Response(HTTP_204, content="")

    app = App(
        components=[MailTemplatesComponent("./tests/mail_templates")],
        routes=[Route("/", template_handler, name="index")],
    )

    client = testing.TestClient(app)

    # Given that a handler will use templating
    # When constructing and email
    response = client.get(app.reverse_uri("index"))

    # Then I should get back a successfull response
    assert response.status_code == 204

    # and the handler should have called the Mail object
    # with the string value of the rendered template
    mail_msg = mail.send.call_args[0][0]
    assert "<th>Hey there Molten</th>" in mail_msg.html
Esempio n. 23
0
def test_missing_auth_cookie(app_settings):
    def test_cookie(jwt_identity: JWTIdentity):
        if jwt_identity is None:
            return "Didn't work"
        return f"Hello {jwt_identity.name} your sub id is {jwt_identity.sub}"

    routes = [Route("/cookie", method="GET", handler=test_cookie)]

    components = [
        SettingsComponent({
            **app_settings,
            **{
                "JWT_AUTH_COOKIE": "molten_auth_cookie"
            }
        }),
        JWTComponent(),
        JWTIdentityComponent(),
    ]

    app = App(routes=routes, components=components)
    client = testing.TestClient(app)

    cookie_response = client.get("/cookie")
    assert "Didn't work" in cookie_response.data
Esempio n. 24
0
    while not ws.closed:
        message = ws.receive()
        if isinstance(message, CloseMessage):
            return

        ws.send(message)


app = App(
    middleware=[
        ResponseRendererMiddleware(),
        WebsocketsMiddleware(),
    ],

    routes=[
        Route("/", index),
        Route("/echo", echo),
    ]
)

client = WebsocketsTestClient(app)


def test_ws_routes_return_bad_request_if_upgrade_is_not_requested():
    # Given that I have a ws endpoint
    # When I make a standard HTTP request to that endpoint
    response = client.get("/echo")

    # Then I should get back a Bad Request response
    assert response.status_code == 400
Esempio n. 25
0
def get_chores(chore_manager: ChoreDefinitionManager) -> Chore:
    chores = chore_manager.get_chores()
    return [Chore.from_chore_model(chore_model) for chore_model in chores]


def create_chore(chore: Chore, chore_manager: ChoreDefinitionManager) -> Chore:
    chore_model = chore_manager.persist_chore(chore)
    return Chore.from_chore_model(chore_model)


def delete_chore(chore_id: str, chore_manager: ChoreDefinitionManager) -> str:
    chore_manager.delete_chore(chore_id)
    return ''


routes = [
    Route('/upcoming', get_upcoming, method='GET', name='get-upcoming'),
    Route('/upcoming/{chore_id}',
          complete_upcoming,
          method='POST',
          name='complete-upcoming'),
    Route('/chores', get_chores, method='GET', name='get-chores'),
    Route('/chores', create_chore, method='POST', name='create-chore'),
    Route('/chores/{chore_id}', get_chore, method='GET', name='get-chore'),
    Route('/chores/{chore_id}',
          delete_chore,
          method='DELETE',
          name='delete-chore'),
]
Esempio n. 26
0
from molten import App, Route


def hello(name: str, age: int) -> str:
    return f"Hello {age} year old named {name}!"


app = App(routes=[Route("/hello/{name}/{age}", hello)])
Esempio n. 27
0
from molten import App, JSONRenderer, QueryParam, ResponseRendererMiddleware, Route
from molten.contrib.sessions import CookieStore, Session, SessionComponent, SessionMiddleware

cookie_store = CookieStore(b"ubersecret")


def set_username(username: QueryParam, session: Session) -> str:
    session["username"] = username
    return username


def get_username(session: Session) -> Optional[str]:
    return session.get("username")


app = App(
    components=[
        SessionComponent(cookie_store),
    ],
    middleware=[
        SessionMiddleware(cookie_store),
        ResponseRendererMiddleware([
            JSONRenderer(),
        ]),
    ],
    routes=[
        Route("/get-username", get_username),
        Route("/set-username", set_username),
    ],
)
Esempio n. 28
0
        "MAIL_PASSWORD": "******",
        "MAIL_PORT": 587,
        "MAIL_USE_TLS": True,
        "MAIL_DEFAULT_SENDER": "*****@*****.**",
    }
)


def send_message(params: QueryParams, mail: Mail):
    """Emails an email address provided in the query string"""
    addresses = params.get_all("email")
    if not addresses:
        return Response(
            HTTP_400,
            content="Provide emails in the query params to send a welcome message",
        )
    msg = Message(
        subject="Welcome to Molten!",
        body="Welcome to Molten! Glad to have you here.",
        recipients=addresses,
    )
    mail.send(msg)
    return Response(HTTP_204, content="")


routes = [Route("/", send_message, "POST")]

components = [SettingsComponent(settings), MailComponent()]

app = App(routes=routes, components=components)
Esempio n. 29
0
    title="Emeeting API",
    description="An API for managing your room meetings.",
    version="0.0.1",
))

get_docs = OpenAPIUIHandler()
"""
Add middlewares
"""

middlewares = [prometheus_middleware, ResponseRendererMiddleware()]
"""
Include or add routes
"""

routes = [
    Route("/", get_docs),
    Route("/schema", get_schema),
    Route("/metrics", expose_metrics),
    Include("/v1/rooms", routes=room.routes),
    Include("/v1/meetings", routes=meeting.routes),
]
"""
Start application
"""

app = App(routes=routes, middleware=middlewares)
app = CORS(app, headers="*", methods="*", origin="*", maxage="86400")

log.info("Start application successfully.")
Esempio n. 30
0
def delete_todo(todo_id: int, todo_manager: TodoManager):
    todo_manager.delete_todo(todo_id)
    return (
        HTTP_202,
        APIResponse(status=202, message=f"Delete request for todo: {todo_id} accepted"),
    )


def get_todo_by_id(todo_id: int, todo_manager: TodoManager) -> Todo:
    try:
        _todo = todo_manager.get_todo_by_id(todo_id)
    except EntityNotFound as err:
        raise HTTPError(HTTP_404,
                        APIResponse(status=404,
                                    message=err.message)
                        )
    return _todo


def update_todo(todo_id: int, todo: Todo, todo_manager: TodoManager) -> Todo:
    return todo_manager.update_todo(todo_id, todo)


todo_routes = Include("/todos", [
    Route("", list_todos, method="GET"),
    Route("", create_todo, method="POST"),
    Route("/{todo_id}", delete_todo, method="DELETE"),
    Route("/{todo_id}", get_todo_by_id, method="GET"),
    Route("/{todo_id}", update_todo, method="PATCH")
])