Skip to content
Snippets Groups Projects
Unverified Commit 9df2b5d6 authored by karp's avatar karp
Browse files

Add missing type declarations

parent 3807efc6
No related branches found
No related tags found
1 merge request!22Make MyPy happy
This commit is part of merge request !22. Comments created here will be created in the context of that merge request.
# Taken and adapted from the flaskr tutorial. Database logic adapted from Flask SQLAlchemy Documentation # Taken and adapted from the flaskr tutorial. Database logic adapted from Flask SQLAlchemy Documentation
# (https://flask-sqlalchemy.palletsprojects.com/en/2.x/contexts/). See LICENSE-3RD-PARTY.md for details. # (https://flask-sqlalchemy.palletsprojects.com/en/2.x/contexts/). See LICENSE-3RD-PARTY.md for details.
import secrets import secrets
from typing import Optional from typing import Any, Optional
from pathlib import Path from pathlib import Path
from flask import Flask from flask import Flask
...@@ -9,7 +9,7 @@ from flask_sqlalchemy import SQLAlchemy ...@@ -9,7 +9,7 @@ from flask_sqlalchemy import SQLAlchemy
from .db_config import get_database_uri, get_uri_for_sqlite from .db_config import get_database_uri, get_uri_for_sqlite
db = SQLAlchemy() db: Any = SQLAlchemy()
from .model import * from .model import *
......
...@@ -7,7 +7,7 @@ from ..model import Key ...@@ -7,7 +7,7 @@ from ..model import Key
@bp.get("/agendas/-/items") @bp.get("/agendas/-/items")
@require_valid(Key) @require_valid(Key)
def the_agenda_items(): def the_agenda_items() -> list[dict[str, str]]:
tasks = sorted( tasks = sorted(
TaigaUserStoryProvider.get_the_one().fetch_user_stories(), TaigaUserStoryProvider.get_the_one().fetch_user_stories(),
key=lambda task: task["kanban_order"], key=lambda task: task["kanban_order"],
......
# Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details) # Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details)
# See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/ # See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/
import functools import functools
from typing import Any, Callable
from flask import request, g from flask import g, request
from portal import db from portal import db
from ..model import Key, User
from ..problem_details import ProblemResponse, not_found, unauthorized
from .blueprint import bp from .blueprint import bp
from ..model import User, Key
from ..problem_details import not_found, unauthorized
@bp.before_app_request @bp.before_app_request
def set_user_if_valid(): def set_user_if_valid() -> None:
if not request.authorization or request.authorization.type != "basic": if (
not request.authorization
or request.authorization.username is None
or request.authorization.password is None
or request.authorization.type != "basic"
):
g.user = None g.user = None
return return
...@@ -23,15 +29,19 @@ def set_user_if_valid(): ...@@ -23,15 +29,19 @@ def set_user_if_valid():
@bp.before_app_request @bp.before_app_request
def set_key_if_valid(): def set_key_if_valid() -> None:
if not request.authorization or request.authorization.type != "bearer": if (
not request.authorization
or request.authorization.token is None
or request.authorization.type != "bearer"
):
g.key = None g.key = None
return return
g.key = Key.get_by_secret_unless_expired(request.authorization.token) g.key = Key.get_by_secret_unless_expired(request.authorization.token)
def require_valid(credential_type): def require_valid(credential_type: type[User | Key]) -> Callable:
""" """
Requires that valid credentials are provided before allowing access to a route. Requires that valid credentials are provided before allowing access to a route.
If no valid credentials are provided, abort and send a 401. If no valid credentials are provided, abort and send a 401.
...@@ -41,9 +51,9 @@ def require_valid(credential_type): ...@@ -41,9 +51,9 @@ def require_valid(credential_type):
- `@require_valid(Key)` to require a valid key - `@require_valid(Key)` to require a valid key
""" """
def decorator(route): def decorator(route: Callable) -> Callable:
@functools.wraps(route) @functools.wraps(route)
def protected_route(**kwargs): def protected_route(**kwargs: Any) -> ProblemResponse:
# Determine which credentials to use # Determine which credentials to use
credentials = {User: g.user, Key: g.key}.get(credential_type) credentials = {User: g.user, Key: g.key}.get(credential_type)
...@@ -60,14 +70,14 @@ def require_valid(credential_type): ...@@ -60,14 +70,14 @@ def require_valid(credential_type):
@bp.post("/keys") @bp.post("/keys")
@require_valid(User) @require_valid(User)
def post_key(): def post_key() -> tuple[dict, int]:
key, secret = Key.generate(g.user) key, secret = Key.generate(g.user)
return key.to_dict() | {"secret": secret}, 201 return key.to_dict() | {"secret": secret}, 201
@bp.delete("/keys/<string:uuid>") @bp.delete("/keys/<string:uuid>")
@require_valid(Key) @require_valid(Key)
def delete_key(uuid: str): def delete_key(uuid: str) -> tuple[str, int] | ProblemResponse:
key_to_delete = Key.get_only(uuid) key_to_delete = Key.get_only(uuid)
if not key_to_delete: if not key_to_delete:
......
from flask import render_template from flask import render_template
from ..problem_details import ProblemResponse, not_found
from .blueprint import bp from .blueprint import bp
from ..problem_details import not_found
@bp.get("/<path:_>") @bp.get("/<path:_>")
def fallback_not_found(_): def fallback_not_found(_: object) -> ProblemResponse:
""" """
If you can't find a matching API endpoint, fallback to returning a 404. If you can't find a matching API endpoint, fallback to returning a 404.
...@@ -16,5 +16,5 @@ def fallback_not_found(_): ...@@ -16,5 +16,5 @@ def fallback_not_found(_):
@bp.get("/") @bp.get("/")
def index(): def index() -> str:
return render_template("redocly.html") return render_template("redocly.html")
import json import json
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any
from .blueprint import bp from .blueprint import bp
@bp.get("/version") @bp.get("/version")
def get_version(): def get_version() -> dict[str, Any]:
portal_version_path = Path("./PORTAL_VERSION") portal_version_path = Path("./PORTAL_VERSION")
if portal_version_path.is_file(): if portal_version_path.is_file():
with open("PORTAL_VERSION") as f: with open("PORTAL_VERSION") as f:
......
from flask import Blueprint, render_template, redirect, url_for from flask import Blueprint, redirect, render_template, url_for
from werkzeug import Response
bp = Blueprint("main", __name__) bp = Blueprint("main", __name__)
@bp.get("/", defaults={"_": ""}) @bp.get("/", defaults={"_": ""})
@bp.get("/<path:_>") @bp.get("/<path:_>")
def index(_): def index(_: object) -> str:
""" """
Serve the index page at /*, i.e. at any path below / and at / itself. Serve the index page at /*, i.e. at any path below / and at / itself.
...@@ -16,5 +17,5 @@ def index(_): ...@@ -16,5 +17,5 @@ def index(_):
@bp.get("/api/") @bp.get("/api/")
def redirect_to_api(): def redirect_to_api() -> Response:
return redirect(url_for("api.index")) return redirect(url_for("api.index"))
from abc import abstractmethod from abc import abstractmethod
from typing import List, Optional, Any, Self from typing import Any, List, Optional, Self
from flask import g from flask import g
from portal import db from portal import db
......
...@@ -4,15 +4,17 @@ Provide RFC 7807-compliant problem details responses for the API. ...@@ -4,15 +4,17 @@ Provide RFC 7807-compliant problem details responses for the API.
from typing import Optional from typing import Optional
ProblemResponse = tuple[dict, int, dict]
def unauthorized():
def unauthorized() -> ProblemResponse:
""" """
Return a 401 message and status code Return a 401 message and status code
""" """
return _problem_response(title="Unauthorized", status=401) return _problem_response(title="Unauthorized", status=401)
def not_found(): def not_found() -> ProblemResponse:
""" """
Return a 404 message and status code Return a 404 message and status code
""" """
...@@ -24,7 +26,7 @@ def _problem_response( ...@@ -24,7 +26,7 @@ def _problem_response(
status: int, status: int,
type_uri: Optional[str] = None, type_uri: Optional[str] = None,
extensions: Optional[dict] = None, extensions: Optional[dict] = None,
) -> tuple[dict, int, dict]: ) -> ProblemResponse:
type_dict = {"type": type_uri} if type_uri is not None else {} type_dict = {"type": type_uri} if type_uri is not None else {}
title_dict = {"title": title} title_dict = {"title": title}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment