From 63817f1249441a08067dc884a6dd5cc26a795481 Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Thu, 12 Feb 2026 09:35:23 -0800 Subject: [PATCH 1/5] Add type hints to src/palace/manager/api/admin/ code. --- pyproject.toml | 5 - .../admin/admin_authentication_provider.py | 12 +- .../admin/controller/collection_settings.py | 16 +- .../manager/api/admin/controller/dashboard.py | 3 +- .../manager/api/admin/model/custom_lists.py | 10 +- .../password_admin_authentication_provider.py | 26 ++- src/palace/manager/api/admin/routes.py | 210 ++++++++++-------- .../manager/api/admin/template_styles.py | 20 +- src/palace/manager/api/admin/validator.py | 73 ++++-- 9 files changed, 231 insertions(+), 144 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b96e9e52d6..22582a1016 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,11 +75,6 @@ disallow_incomplete_defs = false disallow_untyped_decorators = false disallow_untyped_defs = false module = [ - "palace.manager.api.admin.admin_authentication_provider", - "palace.manager.api.admin.model.custom_lists", - "palace.manager.api.admin.password_admin_authentication_provider", - "palace.manager.api.admin.routes", - "palace.manager.api.admin.validator", "palace.manager.api.annotations", "palace.manager.api.app", "palace.manager.api.authentication.base", diff --git a/src/palace/manager/api/admin/admin_authentication_provider.py b/src/palace/manager/api/admin/admin_authentication_provider.py index 02103857b9..3127e9af0b 100644 --- a/src/palace/manager/api/admin/admin_authentication_provider.py +++ b/src/palace/manager/api/admin/admin_authentication_provider.py @@ -1,9 +1,17 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from palace.manager.sqlalchemy.model.admin import Admin + + class AdminAuthenticationProvider: - def sign_in_template(self, redirect_url): + def sign_in_template(self, redirect_url: str | None) -> str: # Returns HTML to be rendered on the sign in page for # this authentication provider. raise NotImplementedError() - def active_credentials(self, admin): + def active_credentials(self, admin: Admin) -> bool: # Returns True if the admin's credentials are not expired. raise NotImplementedError() diff --git a/src/palace/manager/api/admin/controller/collection_settings.py b/src/palace/manager/api/admin/controller/collection_settings.py index 9af3eaa1b4..5254d80152 100644 --- a/src/palace/manager/api/admin/controller/collection_settings.py +++ b/src/palace/manager/api/admin/controller/collection_settings.py @@ -14,6 +14,7 @@ CANNOT_DELETE_COLLECTION_WITH_CHILDREN, IMPORT_NOT_SUPPORTED, MISSING_COLLECTION, + MISSING_IDENTIFIER, MISSING_PARENT, MISSING_SERVICE, PROTOCOL_DOES_NOT_SUPPORT_PARENTS, @@ -170,13 +171,17 @@ def process_deleted_libraries( reap_unassociated_loans.delay() reap_unassociated_holds.delay() - def process_delete(self, service_id: int) -> Response | ProblemDetail: + def process_delete(self, service_id: int | str) -> Response | ProblemDetail: self.require_system_admin() + try: + sid = int(service_id) if isinstance(service_id, str) else service_id + except ValueError: + return MISSING_SERVICE integration = get_one( self._db, IntegrationConfiguration, - id=service_id, + id=sid, goal=self.registry.goal, ) if not integration: @@ -233,8 +238,13 @@ def process_import(self, service_id: int) -> Response | ProblemDetail: return Response("Import task queued.", 200) def process_collection_self_tests( - self, identifier: int | None + self, identifier: int | str | None ) -> Response | ProblemDetail: + if identifier is not None and isinstance(identifier, str): + try: + identifier = int(identifier) + except ValueError: + return MISSING_IDENTIFIER return self.process_self_tests(identifier) def run_self_tests( diff --git a/src/palace/manager/api/admin/controller/dashboard.py b/src/palace/manager/api/admin/controller/dashboard.py index 367767cc16..7541e35600 100644 --- a/src/palace/manager/api/admin/controller/dashboard.py +++ b/src/palace/manager/api/admin/controller/dashboard.py @@ -14,6 +14,7 @@ from palace.manager.api.local_analytics_exporter import LocalAnalyticsExporter from palace.manager.api.util.flask import get_request_library from palace.manager.sqlalchemy.model.admin import Admin +from palace.manager.util.problem_detail import ProblemDetail class DashboardController(CirculationManagerController): @@ -25,7 +26,7 @@ def stats( def bulk_circulation_events( self, analytics_exporter: LocalAnalyticsExporter | None = None - ) -> tuple[str, str, str, str | None]: + ) -> tuple[str | ProblemDetail, str, str, str | None]: date_format = "%Y-%m-%d" def get_date(field: str) -> date: diff --git a/src/palace/manager/api/admin/model/custom_lists.py b/src/palace/manager/api/admin/model/custom_lists.py index a96230e2a5..49361bd1f9 100644 --- a/src/palace/manager/api/admin/model/custom_lists.py +++ b/src/palace/manager/api/admin/model/custom_lists.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import NonNegativeInt from palace.manager.util.flask_util import CustomBaseModel @@ -11,10 +13,10 @@ class CustomListSharePostResponse(CustomBaseModel): class CustomListPostRequest(CustomBaseModel): name: str id: NonNegativeInt | None = None - entries: list[dict] = [] + entries: list[dict[str, Any]] = [] collections: list[int] = [] - deletedEntries: list[dict] = [] + deletedEntries: list[dict[str, Any]] = [] # For auto updating lists auto_update: bool = False - auto_update_query: dict | None = None - auto_update_facets: dict | None = None + auto_update_query: dict[str, Any] | None = None + auto_update_facets: dict[str, Any] | None = None diff --git a/src/palace/manager/api/admin/password_admin_authentication_provider.py b/src/palace/manager/api/admin/password_admin_authentication_provider.py index 058ef0da31..e93f76cfaa 100644 --- a/src/palace/manager/api/admin/password_admin_authentication_provider.py +++ b/src/palace/manager/api/admin/password_admin_authentication_provider.py @@ -1,5 +1,7 @@ -from flask import render_template, url_for +from flask import Response, render_template, url_for from sqlalchemy.orm.session import Session +from werkzeug.datastructures import ImmutableMultiDict +from werkzeug.wrappers.response import Response as WerkzeugResponse from palace.manager.api.admin.admin_authentication_provider import ( AdminAuthenticationProvider, @@ -28,7 +30,7 @@ def __init__(self, send_email: SendEmailCallable): def get_secret_key(db: Session) -> str: return Key.get_key(db, KeyType.ADMIN_SECRET_KEY, raise_exception=True).value - def sign_in_template(self, redirect): + def sign_in_template(self, redirect: str | None) -> str: password_sign_in_url = url_for("password_auth") forgot_password_url = url_for("admin_forgot_password") return render_template( @@ -44,7 +46,9 @@ def sign_in_template(self, redirect): ) @staticmethod - def forgot_password_template(redirect): + def forgot_password_template( + redirect: str | None | Response | WerkzeugResponse, + ) -> str: forgot_password_url = url_for("admin_forgot_password") return render_template( "admin/auth/forgot-password.html.jinja2", @@ -56,7 +60,11 @@ def forgot_password_template(redirect): ) @staticmethod - def reset_password_template(reset_password_token, admin_id, redirect): + def reset_password_template( + reset_password_token: str, + admin_id: int, + redirect: str | None | Response | WerkzeugResponse, + ) -> str: reset_password_url = url_for( "admin_reset_password", reset_password_token=reset_password_token, @@ -71,7 +79,13 @@ def reset_password_template(reset_password_token, admin_id, redirect): button_style=button_style, ) - def sign_in(self, _db, request={}): + def sign_in( + self, + _db: Session, + request: ImmutableMultiDict[str, str] | dict[str, str | None] | None = None, + ) -> tuple[dict[str, str], str | None] | tuple[ProblemDetail, None]: + if request is None: + request = {} email = request.get("email") password = request.get("password") redirect_url = request.get("redirect") @@ -91,7 +105,7 @@ def sign_in(self, _db, request={}): return INVALID_ADMIN_CREDENTIALS, None - def active_credentials(self, admin): + def active_credentials(self, admin: Admin) -> bool: # Admins who have a password are always active. return True diff --git a/src/palace/manager/api/admin/routes.py b/src/palace/manager/api/admin/routes.py index 289397bd4a..f899cc3712 100644 --- a/src/palace/manager/api/admin/routes.py +++ b/src/palace/manager/api/admin/routes.py @@ -1,7 +1,9 @@ +# Decorators from palace.manager.api.routes and core.app_server are untyped. +# mypy: disallow_untyped_decorators=false from collections.abc import Callable from datetime import timedelta from functools import wraps -from typing import ParamSpec, TypeVar +from typing import Any, ParamSpec, TypeVar import flask from flask import Response, make_response, redirect, request, url_for @@ -32,9 +34,11 @@ T = TypeVar("T") -def allows_admin_auth_setup(f): +def allows_admin_auth_setup( + f: Callable[..., Any], +) -> Callable[..., Any]: @wraps(f) - def decorated(*args, **kwargs): + def decorated(*args: Any, **kwargs: Any) -> Any: setting_up = app.manager.admin_sign_in_controller.admin_auth_providers == [] return f(*args, setting_up=setting_up, **kwargs) @@ -62,9 +66,9 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ProblemDetail: return wrapper -def requires_admin(f): +def requires_admin(f: Callable[..., Any]) -> Callable[..., Any]: @wraps(f) - def decorated(*args, **kwargs): + def decorated(*args: Any, **kwargs: Any) -> Any: if "setting_up" in kwargs: # If the function also requires a CSRF token, # setting_up needs to stay in the arguments for @@ -90,11 +94,11 @@ def decorated(*args, **kwargs): return decorated -def requires_csrf_token(f): +def requires_csrf_token(f: Callable[..., Any]) -> Callable[..., Any]: f.__dict__["requires_csrf_token"] = True @wraps(f) - def decorated(*args, **kwargs): + def decorated(*args: Any, **kwargs: Any) -> Any: if "setting_up" in kwargs: setting_up = kwargs.pop("setting_up") else: @@ -108,9 +112,11 @@ def decorated(*args, **kwargs): return decorated -def returns_json_or_response_or_problem_detail(f): +def returns_json_or_response_or_problem_detail( + f: Callable[..., Any], +) -> Callable[..., Any]: @wraps(f) - def decorated(*args, **kwargs): + def decorated(*args: Any, **kwargs: Any) -> Any: try: v = f(*args, **kwargs) except BaseProblemDetailException as ex: @@ -127,33 +133,33 @@ def decorated(*args, **kwargs): @app.route("/admin/sign_in_with_password", methods=["POST"]) @returns_problem_detail -def password_auth(): +def password_auth() -> Any: return app.manager.admin_sign_in_controller.password_sign_in() @app.route("/admin/sign_in") @returns_problem_detail -def admin_sign_in(): +def admin_sign_in() -> Any: return app.manager.admin_sign_in_controller.sign_in() @app.route("/admin/sign_out") @returns_problem_detail @requires_admin -def admin_sign_out(): +def admin_sign_out() -> Any: return app.manager.admin_sign_in_controller.sign_out() @app.route("/admin/change_password", methods=["POST"]) @returns_problem_detail @requires_admin -def admin_change_password(): +def admin_change_password() -> Any: return app.manager.admin_sign_in_controller.change_password() @app.route("/admin/forgot_password", methods=["GET", "POST"]) @returns_problem_detail -def admin_forgot_password(): +def admin_forgot_password() -> Any: return app.manager.admin_reset_password_controller.forgot_password() @@ -161,9 +167,9 @@ def admin_forgot_password(): "/admin/reset_password//", methods=["GET", "POST"] ) @returns_problem_detail -def admin_reset_password(reset_password_token, admin_id): +def admin_reset_password(reset_password_token: str, admin_id: str) -> Any: return app.manager.admin_reset_password_controller.reset_password( - reset_password_token, admin_id + reset_password_token, int(admin_id) if admin_id.isdigit() else 0 ) @@ -171,7 +177,7 @@ def admin_reset_password(reset_password_token, admin_id): @has_library @returns_problem_detail @requires_admin -def work_details(identifier_type, identifier): +def work_details(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.details(identifier_type, identifier) @@ -181,7 +187,7 @@ def work_details(identifier_type, identifier): @has_library @returns_json_or_response_or_problem_detail @requires_admin -def work_classifications(identifier_type, identifier): +def work_classifications(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.classifications( identifier_type, identifier ) @@ -194,7 +200,7 @@ def work_classifications(identifier_type, identifier): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def work_custom_lists(identifier_type, identifier): +def work_custom_lists(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.custom_lists(identifier_type, identifier) @@ -205,7 +211,7 @@ def work_custom_lists(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def edit(identifier_type, identifier): +def edit(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.edit(identifier_type, identifier) @@ -216,7 +222,7 @@ def edit(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def suppress_for_library(identifier_type, identifier): +def suppress_for_library(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.suppress(identifier_type, identifier) @@ -227,7 +233,7 @@ def suppress_for_library(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def unsuppress_for_library(identifier_type, identifier): +def unsuppress_for_library(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.unsuppress(identifier_type, identifier) @@ -239,7 +245,7 @@ def unsuppress_for_library(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def suppress_deprecated(identifier_type, identifier): +def suppress_deprecated(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.suppress(identifier_type, identifier) @@ -251,7 +257,7 @@ def suppress_deprecated(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def unsuppress_deprecated(identifier_type, identifier): +def unsuppress_deprecated(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.unsuppress(identifier_type, identifier) @@ -260,7 +266,7 @@ def unsuppress_deprecated(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def refresh(identifier_type, identifier): +def refresh(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.refresh_metadata( identifier_type, identifier ) @@ -274,7 +280,7 @@ def refresh(identifier_type, identifier): @returns_problem_detail @requires_admin @requires_csrf_token -def edit_classifications(identifier_type, identifier): +def edit_classifications(identifier_type: str, identifier: str) -> Any: return app.manager.admin_work_controller.edit_classifications( identifier_type, identifier ) @@ -282,25 +288,25 @@ def edit_classifications(identifier_type, identifier): @app.route("/admin/roles") @returns_json_or_response_or_problem_detail -def roles(): +def roles() -> Any: return app.manager.admin_work_controller.roles() @app.route("/admin/languages") @returns_json_or_response_or_problem_detail -def languages(): +def languages() -> Any: return app.manager.admin_work_controller.languages() @app.route("/admin/media") @returns_json_or_response_or_problem_detail -def media(): +def media() -> Any: return app.manager.admin_work_controller.media() @app.route("/admin/rights_status") @returns_json_or_response_or_problem_detail -def rights_status(): +def rights_status() -> Any: return app.manager.admin_work_controller.rights_status() @@ -308,7 +314,7 @@ def rights_status(): @has_library @returns_problem_detail @requires_admin -def suppressed(): +def suppressed() -> Any: """Returns a feed of suppressed works.""" return app.manager.admin_feed_controller.suppressed() @@ -317,7 +323,7 @@ def suppressed(): @has_library @returns_json_or_response_or_problem_detail @requires_admin -def suppressed_search(): +def suppressed_search() -> Any: """Search within suppressed/hidden works.""" return app.manager.admin_feed_controller.suppressed_search() @@ -325,7 +331,7 @@ def suppressed_search(): @app.route("/admin/genres") @returns_json_or_response_or_problem_detail @requires_admin -def genres(): +def genres() -> Any: """Returns a JSON representation of complete genre tree.""" return app.manager.admin_feed_controller.genres() @@ -334,7 +340,7 @@ def genres(): @returns_problem_detail @allows_library @requires_admin -def bulk_circulation_events(): +def bulk_circulation_events() -> Any: """Returns a CSV representation of all circulation events with optional start and end times.""" ( @@ -362,7 +368,7 @@ def bulk_circulation_events(): @app.route("/admin/stats") @returns_json_or_response_or_problem_detail @requires_admin -def stats(): +def stats() -> Any: statistics_response: StatisticsResponse = ( app.manager.admin_dashboard_controller.stats(stats_function=generate_statistics) ) @@ -372,7 +378,7 @@ def stats(): @app.route("/admin/quicksight_embed/") @returns_json_or_response_or_problem_detail @requires_admin -def generate_quicksight_url(dashboard_name: str): +def generate_quicksight_url(dashboard_name: str) -> Any: return app.manager.admin_quicksight_controller.generate_quicksight_url( dashboard_name ) @@ -381,7 +387,7 @@ def generate_quicksight_url(dashboard_name: str): @app.route("/admin/quicksight_embed/names") @returns_json_or_response_or_problem_detail @requires_admin -def get_quicksight_names(): +def get_quicksight_names() -> Any: return app.manager.admin_quicksight_controller.get_dashboard_names() @@ -389,7 +395,7 @@ def get_quicksight_names(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def libraries(): +def libraries() -> Any: return app.manager.admin_library_settings_controller.process_libraries() @@ -397,7 +403,7 @@ def libraries(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def library(library_uuid): +def library(library_uuid: str) -> Any: return app.manager.admin_library_settings_controller.process_delete(library_uuid) @@ -405,7 +411,7 @@ def library(library_uuid): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collections(): +def collections() -> Any: return app.manager.admin_collection_settings_controller.process_collections() @@ -413,7 +419,7 @@ def collections(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection(collection_id): +def collection(collection_id: str) -> Any: return app.manager.admin_collection_settings_controller.process_delete( collection_id ) @@ -438,7 +444,7 @@ def collection_import(collection_id): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection_self_tests(identifier): +def collection_self_tests(identifier: str) -> Any: return ( app.manager.admin_collection_settings_controller.process_collection_self_tests( identifier @@ -451,7 +457,7 @@ def collection_self_tests(identifier): @allows_admin_auth_setup @requires_admin @requires_csrf_token -def individual_admins(): +def individual_admins() -> Any: return ( app.manager.admin_individual_admin_settings_controller.process_individual_admins() ) @@ -461,7 +467,7 @@ def individual_admins(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def individual_admin(email): +def individual_admin(email: str) -> Any: return app.manager.admin_individual_admin_settings_controller.process_delete(email) @@ -469,7 +475,7 @@ def individual_admin(email): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_services(): +def patron_auth_services() -> Any: return ( app.manager.admin_patron_auth_services_controller.process_patron_auth_services() ) @@ -479,8 +485,10 @@ def patron_auth_services(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_service(service_id): - return app.manager.admin_patron_auth_services_controller.process_delete(service_id) +def patron_auth_service(service_id: str) -> Any: + return app.manager.admin_patron_auth_services_controller.process_delete( + int(service_id) + ) @app.route( @@ -489,9 +497,9 @@ def patron_auth_service(service_id): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_self_tests(identifier): +def patron_auth_self_tests(identifier: str) -> Any: return app.manager.admin_patron_auth_services_controller.process_patron_auth_service_self_tests( - identifier + int(identifier) ) @@ -500,7 +508,7 @@ def patron_auth_self_tests(identifier): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lookup_patron(): +def lookup_patron() -> Any: return app.manager.admin_patron_controller.lookup_patron() @@ -509,7 +517,7 @@ def lookup_patron(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def reset_adobe_id(): +def reset_adobe_id() -> Any: return app.manager.admin_patron_controller.reset_adobe_id() @@ -534,7 +542,7 @@ def patron_debug_auth(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_services(): +def metadata_services() -> Any: return app.manager.admin_metadata_services_controller.process_metadata_services() @@ -542,17 +550,19 @@ def metadata_services(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_service(service_id): - return app.manager.admin_metadata_services_controller.process_delete(service_id) +def metadata_service(service_id: str) -> Any: + return app.manager.admin_metadata_services_controller.process_delete( + int(service_id) + ) @app.route("/admin/metadata_service_self_tests/", methods=["GET", "POST"]) @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_service_self_tests(identifier): +def metadata_service_self_tests(identifier: str) -> Any: return app.manager.admin_metadata_services_controller.process_metadata_service_self_tests( - identifier + int(identifier) ) @@ -560,7 +570,7 @@ def metadata_service_self_tests(identifier): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def catalog_services(): +def catalog_services() -> Any: return app.manager.admin_catalog_services_controller.process_catalog_services() @@ -568,15 +578,15 @@ def catalog_services(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def catalog_service(service_id): - return app.manager.admin_catalog_services_controller.process_delete(service_id) +def catalog_service(service_id: str) -> Any: + return app.manager.admin_catalog_services_controller.process_delete(int(service_id)) @app.route("/admin/discovery_services", methods=["GET", "POST"]) @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_services(): +def discovery_services() -> Any: return app.manager.admin_discovery_services_controller.process_discovery_services() @@ -584,15 +594,17 @@ def discovery_services(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_service(service_id): - return app.manager.admin_discovery_services_controller.process_delete(service_id) +def discovery_service(service_id: str) -> Any: + return app.manager.admin_discovery_services_controller.process_delete( + int(service_id) + ) @app.route("/admin/announcements", methods=["GET", "POST"]) @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def announcements_for_all(): +def announcements_for_all() -> Any: return app.manager.admin_announcement_service.process_many() @@ -600,7 +612,7 @@ def announcements_for_all(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_service_library_registrations(): +def discovery_service_library_registrations() -> Any: return ( app.manager.admin_discovery_service_library_registrations_controller.process_discovery_service_library_registrations() ) @@ -611,7 +623,7 @@ def discovery_service_library_registrations(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_lists_post(): +def custom_lists_post() -> Any: return app.manager.admin_custom_lists_controller.custom_lists() @@ -620,7 +632,7 @@ def custom_lists_post(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_lists_get(): +def custom_lists_get() -> Any: return app.manager.admin_custom_lists_controller.custom_lists() @@ -629,8 +641,8 @@ def custom_lists_get(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_get(list_id: int): - return app.manager.admin_custom_lists_controller.custom_list(list_id) +def custom_list_get(list_id: str) -> Any: + return app.manager.admin_custom_lists_controller.custom_list(int(list_id)) @library_route("/admin/custom_list/", methods=["POST"]) @@ -638,8 +650,8 @@ def custom_list_get(list_id: int): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_post(list_id): - return app.manager.admin_custom_lists_controller.custom_list(list_id) +def custom_list_post(list_id: str) -> Any: + return app.manager.admin_custom_lists_controller.custom_list(int(list_id)) @library_route("/admin/custom_list/", methods=["DELETE"]) @@ -647,8 +659,8 @@ def custom_list_post(list_id): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_delete(list_id): - return app.manager.admin_custom_lists_controller.custom_list(list_id) +def custom_list_delete(list_id: str) -> Any: + return app.manager.admin_custom_lists_controller.custom_list(int(list_id)) @library_route("/admin/custom_list//share", methods=["POST"]) @@ -656,9 +668,9 @@ def custom_list_delete(list_id): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_share(list_id: int): +def custom_list_share(list_id: str) -> Any: """Share a custom list with all libraries in the CM that share the collections of this library and works of this list""" - return app.manager.admin_custom_lists_controller.share_locally(list_id) + return app.manager.admin_custom_lists_controller.share_locally(int(list_id)) @library_route("/admin/custom_list//share", methods=["DELETE"]) @@ -666,9 +678,9 @@ def custom_list_share(list_id: int): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_unshare(list_id: int): +def custom_list_unshare(list_id: str) -> Any: """Unshare the list from all libraries, as long as no other library is using the list in its lanes""" - return app.manager.admin_custom_lists_controller.share_locally(list_id) + return app.manager.admin_custom_lists_controller.share_locally(int(list_id)) @library_route("/admin/lanes", methods=["GET", "POST"]) @@ -676,7 +688,7 @@ def custom_list_unshare(list_id: int): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lanes(): +def lanes() -> Any: return app.manager.admin_lanes_controller.lanes() @@ -685,8 +697,8 @@ def lanes(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane(lane_identifier): - return app.manager.admin_lanes_controller.lane(lane_identifier) +def lane(lane_identifier: str) -> Any: + return app.manager.admin_lanes_controller.lane(int(lane_identifier)) @library_route("/admin/lane//show", methods=["POST"]) @@ -694,8 +706,8 @@ def lane(lane_identifier): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane_show(lane_identifier): - return app.manager.admin_lanes_controller.show_lane(lane_identifier) +def lane_show(lane_identifier: str) -> Any: + return app.manager.admin_lanes_controller.show_lane(int(lane_identifier)) @library_route("/admin/lane//hide", methods=["POST"]) @@ -703,8 +715,8 @@ def lane_show(lane_identifier): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane_hide(lane_identifier): - return app.manager.admin_lanes_controller.hide_lane(lane_identifier) +def lane_hide(lane_identifier: str) -> Any: + return app.manager.admin_lanes_controller.hide_lane(int(lane_identifier)) @library_route("/admin/lanes/reset", methods=["POST"]) @@ -712,7 +724,7 @@ def lane_hide(lane_identifier): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def reset_lanes(): +def reset_lanes() -> Any: return app.manager.admin_lanes_controller.reset() @@ -721,7 +733,7 @@ def reset_lanes(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def change_lane_order(): +def change_lane_order() -> Any: return app.manager.admin_lanes_controller.change_order() @@ -729,14 +741,14 @@ def change_lane_order(): @has_library @returns_json_or_response_or_problem_detail @requires_admin -def search_field_values(): +def search_field_values() -> Any: return app.manager.admin_search_controller.search_field_values() @app.route("/admin/diagnostics") @requires_admin @returns_json_or_response_or_problem_detail -def diagnostics(): +def diagnostics() -> Any: return app.manager.timestamps_controller.diagnostics() @@ -747,7 +759,7 @@ def diagnostics(): @allows_library @returns_json_or_response_or_problem_detail @requires_admin -def inventory_report_info(): +def inventory_report_info() -> Any: return app.manager.admin_report_controller.inventory_report_info() @@ -758,7 +770,7 @@ def inventory_report_info(): @allows_library @returns_json_or_response_or_problem_detail @requires_admin -def generate_inventory_report(): +def generate_inventory_report() -> Any: return app.manager.admin_report_controller.generate_inventory_report() @@ -766,21 +778,22 @@ def generate_inventory_report(): @has_library @returns_json_or_response_or_problem_detail @requires_admin -def generate_report(report_key: str): +def generate_report(report_key: str) -> Any: return app.manager.admin_report_controller.generate_report(report_key=report_key) @app.route("/admin/sign_in_again") -def admin_sign_in_again(): +def admin_sign_in_again() -> Any: """Allows an admin with expired credentials to sign back in from a new browser tab so they won't lose changes. """ admin = app.manager.admin_sign_in_controller.authenticated_admin_from_request() csrf_token = app.manager.admin_sign_in_controller.get_csrf_token() + # Mock in tests can make get_csrf_token() return ProblemDetail if ( isinstance(admin, ProblemDetail) or csrf_token is None - or isinstance(csrf_token, ProblemDetail) + or isinstance(csrf_token, ProblemDetail) # type: ignore[unreachable] ): redirect_url = flask.request.url return redirect(url_for("admin_sign_in", redirect=redirect_url, _external=True)) @@ -794,19 +807,24 @@ def admin_sign_in_again(): @app.route("/admin/web/collection/") @app.route("/admin/web/book/") @app.route("/admin/web/") # catchall for single-page URLs -def admin_view(collection=None, book=None, etc=None, **kwargs): +def admin_view( + collection: str | None = None, + book: str | None = None, + etc: str | None = None, + **kwargs: Any, +) -> Any: return app.manager.admin_view_controller(collection, book, path=etc) @app.route("/admin/", strict_slashes=False) -def admin_base(**kwargs): +def admin_base(**kwargs: Any) -> Any: return redirect(url_for("admin_view", _external=True)) @app.route("/admin/libraries/import", strict_slashes=False, methods=["POST"]) @returns_json_or_response_or_problem_detail @requires_basic_auth -def import_libraries(): +def import_libraries() -> Any: """Import multiple libraries from a list of library configurations.""" return app.manager.admin_library_settings_controller.import_libraries() @@ -816,7 +834,7 @@ def import_libraries(): @app.route("/admin/static/") @returns_problem_detail - def admin_static_file(filename): + def admin_static_file(filename: str) -> Any: return StaticFileController.static_file( AdminClientConfig.static_files_directory(), filename ) diff --git a/src/palace/manager/api/admin/template_styles.py b/src/palace/manager/api/admin/template_styles.py index 3d90932031..eaac1b0425 100644 --- a/src/palace/manager/api/admin/template_styles.py +++ b/src/palace/manager/api/admin/template_styles.py @@ -1,4 +1,4 @@ -body_style = """ +body_style: str = """ margin: 10vh auto; font-family: 'Open Sans',Helvetica,Arial,sans-serif; padding: 25px 15px; @@ -11,17 +11,17 @@ align-items: center; """ -label_style = """ +label_style: str = """ font-weight: 700; """ -error_style = ( +error_style: str = ( body_style + """ border-color: #D0343A; """ ) -input_style = """ +input_style: str = """ border-radius: .25em; display: block; padding: 10px; @@ -32,14 +32,14 @@ width: 25vw; """ -section_style = """ +section_style: str = """ width: 25vw; padding: 12px; display: flex; justify-content: space-between; align-items: center; """ -button_style = """ +button_style: str = """ background: #242DAB; border-color: transparent; border-radius: .25em; @@ -53,7 +53,7 @@ margin: 2vh auto; """ -link_style = """ +link_style: str = """ background: #242DAB; text-align: center; text-decoration: none; @@ -68,7 +68,7 @@ margin: 2vh auto; """ -small_link_style = ( +small_link_style: str = ( link_style + """ width: 5vw; @@ -76,14 +76,14 @@ """ ) -hr_style = """ +hr_style: str = """ width: 10vw; margin: 3px 0 0 0; border: none; border-bottom: 1px solid #403d37; """ -logo_style = """ +logo_style: str = """ width: 200px; margin: 20px; """ diff --git a/src/palace/manager/api/admin/validator.py b/src/palace/manager/api/admin/validator.py index 7a024c143e..565dc4b059 100644 --- a/src/palace/manager/api/admin/validator.py +++ b/src/palace/manager/api/admin/validator.py @@ -1,4 +1,10 @@ +from __future__ import annotations + +# mypy: warn_unreachable=false import re +from collections.abc import Callable +from re import Match +from typing import Any from flask_babel import lazy_gettext as _ @@ -7,11 +13,14 @@ INVALID_NUMBER, INVALID_URL, ) +from palace.manager.util.problem_detail import ProblemDetail class Validator: - def validate(self, settings, content): - validators = [ + def validate( + self, settings: list[dict[str, Any]] | str, content: dict[str, Any] + ) -> ProblemDetail | None: + validators: list[Callable[..., ProblemDetail | None],] = [ self.validate_email, self.validate_url, self.validate_number, @@ -21,12 +30,20 @@ def validate(self, settings, content): error = validator(settings, content) if error: return error + return None def _extract_inputs( - self, settings, value, form, key="format", is_list=False, should_zip=False - ): + self, + settings: list[dict[str, Any]], + value: str, + form: dict[str, Any] | None, + key: str = "format", + is_list: bool = False, + should_zip: bool = False, + ) -> list[Any]: if not (isinstance(settings, list)): return [] + form = form or {} fields = [s for s in settings if s.get(key) == value and self._value(s, form)] @@ -40,7 +57,9 @@ def _extract_inputs( else: return values - def validate_email(self, settings, content): + def validate_email( + self, settings: list[dict[str, Any]] | str, content: dict[str, Any] + ) -> ProblemDetail | None: """Find any email addresses that the user has submitted, and make sure that they are in a valid format. This method is used by individual_admin_settings and library_settings. @@ -49,7 +68,9 @@ def validate_email(self, settings, content): # If :param settings is a list of objects--i.e. the LibrarySettingsController # is calling this method--then we need to pull out the relevant input strings # to validate. - email_inputs = self._extract_inputs(settings, "email", content.get("form")) + email_inputs = self._extract_inputs( + settings, "email", content.get("form") or {} + ) else: # If the IndividualAdminSettingsController is calling this method, then we already have the # input string; it was passed in directly. @@ -64,15 +85,20 @@ def validate_email(self, settings, content): return INVALID_EMAIL.detailed( _('"%(email)s" is not a valid email address.', email=email) ) + return None - def _is_email(self, email): + def _is_email(self, email: str) -> Match[str] | None: """Email addresses must be in the format 'x@y.z'.""" email_format = r".+\@.+\..+" return re.search(email_format, email) - def validate_url(self, settings, content): + def validate_url( + self, settings: list[dict[str, Any]] | str, content: dict[str, Any] + ) -> ProblemDetail | None: """Find any URLs that the user has submitted, and make sure that they are in a valid format.""" + if not isinstance(settings, list): + return None # Find the fields that have to do with URLs and are not blank. url_inputs = self._extract_inputs( settings, "url", content.get("form"), should_zip=True @@ -89,9 +115,10 @@ def validate_url(self, settings, content): return INVALID_URL.detailed( _('"%(url)s" is not a valid URL.', url=url) ) + return None @classmethod - def _is_url(cls, url, allowed): + def _is_url(cls, url: str, allowed: list[str]) -> bool: if not url: return False has_protocol = any( @@ -99,9 +126,13 @@ def _is_url(cls, url, allowed): ) return has_protocol or (url in allowed) - def validate_number(self, settings, content): + def validate_number( + self, settings: list[dict[str, Any]] | str, content: dict[str, Any] + ) -> ProblemDetail | None: """Find any numbers that the user has submitted, and make sure that they are 1) actually numbers, 2) positive, and 3) lower than the specified maximum, if there is one.""" + if not isinstance(settings, list): + return None # Find the fields that should have numeric input and are not blank. number_inputs = self._extract_inputs( settings, "number", content.get("form"), key="type", should_zip=True @@ -110,8 +141,9 @@ def validate_number(self, settings, content): error = self._number_error(field, number) if error: return error + return None - def _number_error(self, field, number): + def _number_error(self, field: dict[str, Any], number: Any) -> ProblemDetail | None: min = field.get("min") or 0 max = field.get("max") @@ -138,21 +170,28 @@ def _number_error(self, field, number): max=max, ) ) + return None - def _list_of_values(self, fields, form): - result = [] + def _list_of_values(self, fields: list[dict[str, Any]], form: Any) -> list[Any]: + result: list[Any] = [] for field in fields: result += self._value(field, form) return [_f for _f in result if _f] - def _value(self, field, form): + def _value(self, field: dict[str, Any], form: Any) -> Any: # Extract the user's input for this field. If this is a sitewide setting, # then the input needs to be accessed via "value" rather than via the setting's key. # We use getlist instead of get so that, if the field is such that the user can input multiple values # (e.g. language codes), we'll extract all the values, not just the first one. - value = form.getlist(field.get("key")) + # form is typed as Any because it may be ImmutableMultiDict (has getlist) or dict-like. + getlist = getattr(form, "getlist", None) + if getlist is not None: + value = list(getlist(field.get("key"))) + else: + v = form.get(field.get("key")) + value = [v] if v is not None else [] if not value: return form.get("value") - elif len(value) == 1: + if len(value) == 1: return value[0] - return [x for x in value if x != None and x != ""] + return [x for x in value if x is not None and x != ""] From cf8139617bab838dbd605be48f10b3b7c2d90c86 Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Fri, 13 Feb 2026 12:41:28 -0800 Subject: [PATCH 2/5] Fix broken tests. --- .../api/admin/controller/catalog_services.py | 13 +++++-- .../api/admin/controller/custom_lists.py | 14 ++++++-- .../admin/controller/discovery_services.py | 13 +++++-- .../manager/api/admin/controller/lanes.py | 30 ++++++++++++++-- .../api/admin/controller/metadata_services.py | 21 +++++++++--- .../admin/controller/patron_auth_services.py | 17 ++++++++-- src/palace/manager/api/admin/routes.py | 34 ++++++++----------- 7 files changed, 104 insertions(+), 38 deletions(-) diff --git a/src/palace/manager/api/admin/controller/catalog_services.py b/src/palace/manager/api/admin/controller/catalog_services.py index 3f6abfff8d..2d09feb7c5 100644 --- a/src/palace/manager/api/admin/controller/catalog_services.py +++ b/src/palace/manager/api/admin/controller/catalog_services.py @@ -7,7 +7,10 @@ UpdatedLibrarySettingsTuple, ) from palace.manager.api.admin.form_data import ProcessFormData -from palace.manager.api.admin.problem_details import MULTIPLE_SERVICES_FOR_LIBRARY +from palace.manager.api.admin.problem_details import ( + MISSING_SERVICE, + MULTIPLE_SERVICES_FOR_LIBRARY, +) from palace.manager.integration.catalog.marc.exporter import MarcExporter from palace.manager.integration.goals import Goals from palace.manager.integration.settings import BaseSettings @@ -102,6 +105,10 @@ def process_post(self) -> Response | ProblemDetail: return Response(str(catalog_service.id), response_code) - def process_delete(self, service_id: int) -> Response: + def process_delete(self, service_id: int | str) -> Response | ProblemDetail: self.require_system_admin() - return self.delete_service(service_id) + try: + sid = int(service_id) if isinstance(service_id, str) else service_id + except ValueError: + return MISSING_SERVICE + return self.delete_service(sid) diff --git a/src/palace/manager/api/admin/controller/custom_lists.py b/src/palace/manager/api/admin/controller/custom_lists.py index 260d913080..a11b99259e 100644 --- a/src/palace/manager/api/admin/controller/custom_lists.py +++ b/src/palace/manager/api/admin/controller/custom_lists.py @@ -313,8 +313,12 @@ def url_fn(after: int) -> str: return url_fn def custom_list( - self, list_id: int + self, list_id: int | str ) -> Response | dict[str, Any] | ProblemDetail | None: + try: + list_id = int(list_id) if isinstance(list_id, str) else list_id + except ValueError: + return MISSING_CUSTOM_LIST library = get_request_library() self.require_librarian(library) data_source = DataSource.lookup(self._db, DataSource.LIBRARY_STAFF) @@ -398,9 +402,15 @@ def custom_list( return None def share_locally( - self, customlist_id: int + self, customlist_id: int | str ) -> ProblemDetail | dict[str, int] | Response: """Share this customlist with all libraries on this local CM""" + try: + customlist_id = ( + int(customlist_id) if isinstance(customlist_id, str) else customlist_id + ) + except ValueError: + return MISSING_CUSTOM_LIST if not customlist_id: return INVALID_INPUT customlist = get_one(self._db, CustomList, id=customlist_id) diff --git a/src/palace/manager/api/admin/controller/discovery_services.py b/src/palace/manager/api/admin/controller/discovery_services.py index c8fb4c1b31..504270ee62 100644 --- a/src/palace/manager/api/admin/controller/discovery_services.py +++ b/src/palace/manager/api/admin/controller/discovery_services.py @@ -7,7 +7,10 @@ IntegrationSettingsController, ) from palace.manager.api.admin.form_data import ProcessFormData -from palace.manager.api.admin.problem_details import INTEGRATION_URL_ALREADY_IN_USE +from palace.manager.api.admin.problem_details import ( + INTEGRATION_URL_ALREADY_IN_USE, + MISSING_SERVICE, +) from palace.manager.integration.discovery.opds_registration import ( OpdsRegistrationService, ) @@ -78,10 +81,14 @@ def process_post(self) -> Response | ProblemDetail: return Response(str(service.id), response_code) - def process_delete(self, service_id: int) -> Response | ProblemDetail: + def process_delete(self, service_id: int | str) -> Response | ProblemDetail: self.require_system_admin() try: - return self.delete_service(service_id) + sid = int(service_id) if isinstance(service_id, str) else service_id + except ValueError: + return MISSING_SERVICE + try: + return self.delete_service(sid) except ProblemDetailException as e: self._db.rollback() return e.problem_detail diff --git a/src/palace/manager/api/admin/controller/lanes.py b/src/palace/manager/api/admin/controller/lanes.py index 9dc909a387..7e29532eb6 100644 --- a/src/palace/manager/api/admin/controller/lanes.py +++ b/src/palace/manager/api/admin/controller/lanes.py @@ -165,7 +165,15 @@ def lanes_for_parent(parent: Lane | None) -> list[dict[str, Any]]: return Response(str(lane.id), 200) raise RuntimeError("Unsupported method") - def lane(self, lane_identifier: int) -> Response | ProblemDetail: + def lane(self, lane_identifier: int | str) -> Response | ProblemDetail: + try: + lane_identifier = ( + int(lane_identifier) + if isinstance(lane_identifier, str) + else lane_identifier + ) + except ValueError: + return MISSING_LANE if flask.request.method == "DELETE": library = get_request_library() self.require_library_manager(library) @@ -197,7 +205,15 @@ def _check_lane_name_unique( LANE_WITH_PARENT_AND_DISPLAY_NAME_ALREADY_EXISTS ) - def show_lane(self, lane_identifier: int) -> Response | ProblemDetail: + def show_lane(self, lane_identifier: int | str) -> Response | ProblemDetail: + try: + lane_identifier = ( + int(lane_identifier) + if isinstance(lane_identifier, str) + else lane_identifier + ) + except ValueError: + return MISSING_LANE library = get_request_library() self.require_library_manager(library) @@ -209,7 +225,15 @@ def show_lane(self, lane_identifier: int) -> Response | ProblemDetail: lane.visible = True return Response(str(_("Success")), 200) - def hide_lane(self, lane_identifier: int) -> Response | ProblemDetail: + def hide_lane(self, lane_identifier: int | str) -> Response | ProblemDetail: + try: + lane_identifier = ( + int(lane_identifier) + if isinstance(lane_identifier, str) + else lane_identifier + ) + except ValueError: + return MISSING_LANE library = get_request_library() self.require_library_manager(library) diff --git a/src/palace/manager/api/admin/controller/metadata_services.py b/src/palace/manager/api/admin/controller/metadata_services.py index 0ed40cbf1c..0369a3b0ca 100644 --- a/src/palace/manager/api/admin/controller/metadata_services.py +++ b/src/palace/manager/api/admin/controller/metadata_services.py @@ -8,7 +8,11 @@ IntegrationSettingsSelfTestsController, ) from palace.manager.api.admin.form_data import ProcessFormData -from palace.manager.api.admin.problem_details import DUPLICATE_INTEGRATION +from palace.manager.api.admin.problem_details import ( + DUPLICATE_INTEGRATION, + MISSING_IDENTIFIER, + MISSING_SERVICE, +) from palace.manager.core.selftest import HasSelfTests from palace.manager.integration.base import HasLibraryIntegrationConfiguration from palace.manager.integration.metadata.base import MetadataServiceType @@ -85,9 +89,13 @@ def process_post(self) -> Response | ProblemDetail: return Response(str(metadata_service.id), response_code) - def process_delete(self, service_id: int) -> Response: + def process_delete(self, service_id: int | str) -> Response | ProblemDetail: self.require_system_admin() - return self.delete_service(service_id) + try: + sid = int(service_id) if isinstance(service_id, str) else service_id + except ValueError: + return MISSING_SERVICE + return self.delete_service(sid) def run_self_tests( self, integration: IntegrationConfiguration @@ -103,6 +111,11 @@ def run_self_tests( return None def process_metadata_service_self_tests( - self, identifier: int | None + self, identifier: int | str | None ) -> Response | ProblemDetail: + if identifier is not None and isinstance(identifier, str): + try: + identifier = int(identifier) + except ValueError: + return MISSING_IDENTIFIER return self.process_self_tests(identifier) diff --git a/src/palace/manager/api/admin/controller/patron_auth_services.py b/src/palace/manager/api/admin/controller/patron_auth_services.py index a652f591d3..9d346cc697 100644 --- a/src/palace/manager/api/admin/controller/patron_auth_services.py +++ b/src/palace/manager/api/admin/controller/patron_auth_services.py @@ -13,6 +13,8 @@ from palace.manager.api.admin.form_data import ProcessFormData from palace.manager.api.admin.problem_details import ( FAILED_TO_RUN_SELF_TESTS, + MISSING_IDENTIFIER, + MISSING_SERVICE, MULTIPLE_BASIC_AUTH_SERVICES, ) from palace.manager.api.authentication.base import AuthenticationProviderType @@ -120,17 +122,26 @@ def process_updated_libraries( for integration, _ in libraries: self.library_integration_validation(integration) - def process_delete(self, service_id: int) -> Response | ProblemDetail: + def process_delete(self, service_id: int | str) -> Response | ProblemDetail: self.require_system_admin() try: - return self.delete_service(service_id) + sid = int(service_id) if isinstance(service_id, str) else service_id + except ValueError: + return MISSING_SERVICE + try: + return self.delete_service(sid) except ProblemDetailException as e: self._db.rollback() return e.problem_detail def process_patron_auth_service_self_tests( - self, identifier: int | None + self, identifier: int | str | None ) -> Response | ProblemDetail: + if identifier is not None and isinstance(identifier, str): + try: + identifier = int(identifier) + except ValueError: + return MISSING_IDENTIFIER return self.process_self_tests(identifier) def get_prior_test_results( diff --git a/src/palace/manager/api/admin/routes.py b/src/palace/manager/api/admin/routes.py index f899cc3712..c1a21ebf71 100644 --- a/src/palace/manager/api/admin/routes.py +++ b/src/palace/manager/api/admin/routes.py @@ -486,9 +486,7 @@ def patron_auth_services() -> Any: @requires_admin @requires_csrf_token def patron_auth_service(service_id: str) -> Any: - return app.manager.admin_patron_auth_services_controller.process_delete( - int(service_id) - ) + return app.manager.admin_patron_auth_services_controller.process_delete(service_id) @app.route( @@ -499,7 +497,7 @@ def patron_auth_service(service_id: str) -> Any: @requires_csrf_token def patron_auth_self_tests(identifier: str) -> Any: return app.manager.admin_patron_auth_services_controller.process_patron_auth_service_self_tests( - int(identifier) + identifier ) @@ -551,9 +549,7 @@ def metadata_services() -> Any: @requires_admin @requires_csrf_token def metadata_service(service_id: str) -> Any: - return app.manager.admin_metadata_services_controller.process_delete( - int(service_id) - ) + return app.manager.admin_metadata_services_controller.process_delete(service_id) @app.route("/admin/metadata_service_self_tests/", methods=["GET", "POST"]) @@ -562,7 +558,7 @@ def metadata_service(service_id: str) -> Any: @requires_csrf_token def metadata_service_self_tests(identifier: str) -> Any: return app.manager.admin_metadata_services_controller.process_metadata_service_self_tests( - int(identifier) + identifier ) @@ -579,7 +575,7 @@ def catalog_services() -> Any: @requires_admin @requires_csrf_token def catalog_service(service_id: str) -> Any: - return app.manager.admin_catalog_services_controller.process_delete(int(service_id)) + return app.manager.admin_catalog_services_controller.process_delete(service_id) @app.route("/admin/discovery_services", methods=["GET", "POST"]) @@ -595,9 +591,7 @@ def discovery_services() -> Any: @requires_admin @requires_csrf_token def discovery_service(service_id: str) -> Any: - return app.manager.admin_discovery_services_controller.process_delete( - int(service_id) - ) + return app.manager.admin_discovery_services_controller.process_delete(service_id) @app.route("/admin/announcements", methods=["GET", "POST"]) @@ -642,7 +636,7 @@ def custom_lists_get() -> Any: @requires_admin @requires_csrf_token def custom_list_get(list_id: str) -> Any: - return app.manager.admin_custom_lists_controller.custom_list(int(list_id)) + return app.manager.admin_custom_lists_controller.custom_list(list_id) @library_route("/admin/custom_list/", methods=["POST"]) @@ -651,7 +645,7 @@ def custom_list_get(list_id: str) -> Any: @requires_admin @requires_csrf_token def custom_list_post(list_id: str) -> Any: - return app.manager.admin_custom_lists_controller.custom_list(int(list_id)) + return app.manager.admin_custom_lists_controller.custom_list(list_id) @library_route("/admin/custom_list/", methods=["DELETE"]) @@ -660,7 +654,7 @@ def custom_list_post(list_id: str) -> Any: @requires_admin @requires_csrf_token def custom_list_delete(list_id: str) -> Any: - return app.manager.admin_custom_lists_controller.custom_list(int(list_id)) + return app.manager.admin_custom_lists_controller.custom_list(list_id) @library_route("/admin/custom_list//share", methods=["POST"]) @@ -670,7 +664,7 @@ def custom_list_delete(list_id: str) -> Any: @requires_csrf_token def custom_list_share(list_id: str) -> Any: """Share a custom list with all libraries in the CM that share the collections of this library and works of this list""" - return app.manager.admin_custom_lists_controller.share_locally(int(list_id)) + return app.manager.admin_custom_lists_controller.share_locally(list_id) @library_route("/admin/custom_list//share", methods=["DELETE"]) @@ -680,7 +674,7 @@ def custom_list_share(list_id: str) -> Any: @requires_csrf_token def custom_list_unshare(list_id: str) -> Any: """Unshare the list from all libraries, as long as no other library is using the list in its lanes""" - return app.manager.admin_custom_lists_controller.share_locally(int(list_id)) + return app.manager.admin_custom_lists_controller.share_locally(list_id) @library_route("/admin/lanes", methods=["GET", "POST"]) @@ -698,7 +692,7 @@ def lanes() -> Any: @requires_admin @requires_csrf_token def lane(lane_identifier: str) -> Any: - return app.manager.admin_lanes_controller.lane(int(lane_identifier)) + return app.manager.admin_lanes_controller.lane(lane_identifier) @library_route("/admin/lane//show", methods=["POST"]) @@ -707,7 +701,7 @@ def lane(lane_identifier: str) -> Any: @requires_admin @requires_csrf_token def lane_show(lane_identifier: str) -> Any: - return app.manager.admin_lanes_controller.show_lane(int(lane_identifier)) + return app.manager.admin_lanes_controller.show_lane(lane_identifier) @library_route("/admin/lane//hide", methods=["POST"]) @@ -716,7 +710,7 @@ def lane_show(lane_identifier: str) -> Any: @requires_admin @requires_csrf_token def lane_hide(lane_identifier: str) -> Any: - return app.manager.admin_lanes_controller.hide_lane(int(lane_identifier)) + return app.manager.admin_lanes_controller.hide_lane(lane_identifier) @library_route("/admin/lanes/reset", methods=["POST"]) From 1379c6acdae7637ba024f8f9f8e13c114d717bee Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Tue, 3 Mar 2026 12:10:15 -0800 Subject: [PATCH 3/5] Address PR review comments. --- .../api/admin/controller/reset_password.py | 10 +++-- .../password_admin_authentication_provider.py | 7 ++-- src/palace/manager/api/admin/routes.py | 39 +++++++++---------- .../manager/api/admin/template_styles.py | 20 +++++----- src/palace/manager/api/admin/validator.py | 13 +++---- src/palace/manager/api/routes.py | 30 ++++++++++---- src/palace/manager/core/app_server.py | 14 +++++-- 7 files changed, 77 insertions(+), 56 deletions(-) diff --git a/src/palace/manager/api/admin/controller/reset_password.py b/src/palace/manager/api/admin/controller/reset_password.py index 2032a5a8e9..70f91ca621 100644 --- a/src/palace/manager/api/admin/controller/reset_password.py +++ b/src/palace/manager/api/admin/controller/reset_password.py @@ -43,13 +43,14 @@ def forgot_password(self) -> ProblemDetail | WerkzeugResponse: logged_in_admin = self.authenticated_admin_from_request() - admin_view_redirect = redirect(url_for("admin_view")) + admin_view_url = url_for("admin_view") + admin_view_redirect = redirect(admin_view_url) if isinstance(logged_in_admin, Admin): return admin_view_redirect if flask.request.method == "GET": - auth_provider_html = auth.forgot_password_template(admin_view_redirect) + auth_provider_html = auth.forgot_password_template(admin_view_url) html = self.FORGOT_PASSWORD_TEMPLATE % dict( auth_provider_html=auth_provider_html, @@ -118,7 +119,8 @@ def reset_password( logged_in_admin = self.authenticated_admin_from_request() - admin_view_redirect = redirect(url_for("admin_view")) + admin_view_url = url_for("admin_view") + admin_view_redirect = redirect(admin_view_url) # If the admin is logged in we redirect it since in that case the logged in change password option can be used if isinstance(logged_in_admin, Admin): @@ -139,7 +141,7 @@ def reset_password( if flask.request.method == "GET": auth_provider_html = auth.reset_password_template( - reset_password_token, admin_id, admin_view_redirect + reset_password_token, admin_id, admin_view_url ) html = self.RESET_PASSWORD_TEMPLATE % dict( diff --git a/src/palace/manager/api/admin/password_admin_authentication_provider.py b/src/palace/manager/api/admin/password_admin_authentication_provider.py index e93f76cfaa..a2666c17ee 100644 --- a/src/palace/manager/api/admin/password_admin_authentication_provider.py +++ b/src/palace/manager/api/admin/password_admin_authentication_provider.py @@ -1,7 +1,6 @@ -from flask import Response, render_template, url_for +from flask import render_template, url_for from sqlalchemy.orm.session import Session from werkzeug.datastructures import ImmutableMultiDict -from werkzeug.wrappers.response import Response as WerkzeugResponse from palace.manager.api.admin.admin_authentication_provider import ( AdminAuthenticationProvider, @@ -47,7 +46,7 @@ def sign_in_template(self, redirect: str | None) -> str: @staticmethod def forgot_password_template( - redirect: str | None | Response | WerkzeugResponse, + redirect: str | None, ) -> str: forgot_password_url = url_for("admin_forgot_password") return render_template( @@ -63,7 +62,7 @@ def forgot_password_template( def reset_password_template( reset_password_token: str, admin_id: int, - redirect: str | None | Response | WerkzeugResponse, + redirect: str | None, ) -> str: reset_password_url = url_for( "admin_reset_password", diff --git a/src/palace/manager/api/admin/routes.py b/src/palace/manager/api/admin/routes.py index c1a21ebf71..3395598d04 100644 --- a/src/palace/manager/api/admin/routes.py +++ b/src/palace/manager/api/admin/routes.py @@ -1,5 +1,3 @@ -# Decorators from palace.manager.api.routes and core.app_server are untyped. -# mypy: disallow_untyped_decorators=false from collections.abc import Callable from datetime import timedelta from functools import wraps @@ -34,13 +32,12 @@ T = TypeVar("T") -def allows_admin_auth_setup( - f: Callable[..., Any], -) -> Callable[..., Any]: +def allows_admin_auth_setup[**P, T](f: Callable[P, T]) -> Callable[P, T]: @wraps(f) - def decorated(*args: Any, **kwargs: Any) -> Any: + def decorated(*args: P.args, **kwargs: P.kwargs) -> T: setting_up = app.manager.admin_sign_in_controller.admin_auth_providers == [] - return f(*args, setting_up=setting_up, **kwargs) + kwargs["setting_up"] = setting_up + return f(*args, **kwargs) return decorated @@ -66,9 +63,9 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ProblemDetail: return wrapper -def requires_admin(f: Callable[..., Any]) -> Callable[..., Any]: +def requires_admin[**P, T](f: Callable[P, T]) -> Callable[P, T | Response]: @wraps(f) - def decorated(*args: Any, **kwargs: Any) -> Any: + def decorated(*args: P.args, **kwargs: P.kwargs) -> T | Response: if "setting_up" in kwargs: # If the function also requires a CSRF token, # setting_up needs to stay in the arguments for @@ -94,11 +91,11 @@ def decorated(*args: Any, **kwargs: Any) -> Any: return decorated -def requires_csrf_token(f: Callable[..., Any]) -> Callable[..., Any]: +def requires_csrf_token[**P, T](f: Callable[P, T]) -> Callable[P, T | ProblemDetail]: f.__dict__["requires_csrf_token"] = True @wraps(f) - def decorated(*args: Any, **kwargs: Any) -> Any: + def decorated(*args: P.args, **kwargs: P.kwargs) -> T | ProblemDetail: if "setting_up" in kwargs: setting_up = kwargs.pop("setting_up") else: @@ -112,21 +109,23 @@ def decorated(*args: Any, **kwargs: Any) -> Any: return decorated -def returns_json_or_response_or_problem_detail( - f: Callable[..., Any], -) -> Callable[..., Any]: +def returns_json_or_response_or_problem_detail[**P, T]( + f: Callable[P, T], +) -> Callable[P, Response | tuple[str, int, dict[str, str]]]: @wraps(f) - def decorated(*args: Any, **kwargs: Any) -> Any: + def decorated( + *args: P.args, **kwargs: P.kwargs + ) -> Response | tuple[str, int, dict[str, str]]: try: v = f(*args, **kwargs) except BaseProblemDetailException as ex: # A ProblemDetailException just needs to be converted to a ProblemDetail. - v = ex.problem_detail + v = ex.problem_detail # type: ignore[assignment] if isinstance(v, ProblemDetail): return v.response if isinstance(v, Response): return v - return flask.jsonify(**v) + return flask.jsonify(**v) # type: ignore[arg-type] return decorated @@ -429,7 +428,7 @@ def collection(collection_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection_import(collection_id): +def collection_import(collection_id: str) -> Any: try: integration_id = int(collection_id) except ValueError: @@ -523,7 +522,7 @@ def reset_adobe_id() -> Any: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def patron_auth_methods(): +def patron_auth_methods() -> Any: return app.manager.admin_patron_controller.get_auth_methods() @@ -532,7 +531,7 @@ def patron_auth_methods(): @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_debug_auth(): +def patron_debug_auth() -> Any: return app.manager.admin_patron_controller.debug_auth() diff --git a/src/palace/manager/api/admin/template_styles.py b/src/palace/manager/api/admin/template_styles.py index eaac1b0425..3d90932031 100644 --- a/src/palace/manager/api/admin/template_styles.py +++ b/src/palace/manager/api/admin/template_styles.py @@ -1,4 +1,4 @@ -body_style: str = """ +body_style = """ margin: 10vh auto; font-family: 'Open Sans',Helvetica,Arial,sans-serif; padding: 25px 15px; @@ -11,17 +11,17 @@ align-items: center; """ -label_style: str = """ +label_style = """ font-weight: 700; """ -error_style: str = ( +error_style = ( body_style + """ border-color: #D0343A; """ ) -input_style: str = """ +input_style = """ border-radius: .25em; display: block; padding: 10px; @@ -32,14 +32,14 @@ width: 25vw; """ -section_style: str = """ +section_style = """ width: 25vw; padding: 12px; display: flex; justify-content: space-between; align-items: center; """ -button_style: str = """ +button_style = """ background: #242DAB; border-color: transparent; border-radius: .25em; @@ -53,7 +53,7 @@ margin: 2vh auto; """ -link_style: str = """ +link_style = """ background: #242DAB; text-align: center; text-decoration: none; @@ -68,7 +68,7 @@ margin: 2vh auto; """ -small_link_style: str = ( +small_link_style = ( link_style + """ width: 5vw; @@ -76,14 +76,14 @@ """ ) -hr_style: str = """ +hr_style = """ width: 10vw; margin: 3px 0 0 0; border: none; border-bottom: 1px solid #403d37; """ -logo_style: str = """ +logo_style = """ width: 200px; margin: 20px; """ diff --git a/src/palace/manager/api/admin/validator.py b/src/palace/manager/api/admin/validator.py index 565dc4b059..d62c969bb2 100644 --- a/src/palace/manager/api/admin/validator.py +++ b/src/palace/manager/api/admin/validator.py @@ -1,6 +1,5 @@ from __future__ import annotations -# mypy: warn_unreachable=false import re from collections.abc import Callable from re import Match @@ -41,8 +40,6 @@ def _extract_inputs( is_list: bool = False, should_zip: bool = False, ) -> list[Any]: - if not (isinstance(settings, list)): - return [] form = form or {} fields = [s for s in settings if s.get(key) == value and self._value(s, form)] @@ -172,23 +169,25 @@ def _number_error(self, field: dict[str, Any], number: Any) -> ProblemDetail | N ) return None - def _list_of_values(self, fields: list[dict[str, Any]], form: Any) -> list[Any]: + def _list_of_values( + self, fields: list[dict[str, Any]], form: dict[str, Any] + ) -> list[Any]: result: list[Any] = [] for field in fields: result += self._value(field, form) return [_f for _f in result if _f] - def _value(self, field: dict[str, Any], form: Any) -> Any: + def _value(self, field: dict[str, Any], form: dict[str, Any]) -> Any: # Extract the user's input for this field. If this is a sitewide setting, # then the input needs to be accessed via "value" rather than via the setting's key. # We use getlist instead of get so that, if the field is such that the user can input multiple values # (e.g. language codes), we'll extract all the values, not just the first one. - # form is typed as Any because it may be ImmutableMultiDict (has getlist) or dict-like. getlist = getattr(form, "getlist", None) if getlist is not None: value = list(getlist(field.get("key"))) else: - v = form.get(field.get("key")) + field_key = field.get("key") + v = form.get(field_key) if isinstance(field_key, str) else None value = [v] if v is not None else [] if not value: return form.get("value") diff --git a/src/palace/manager/api/routes.py b/src/palace/manager/api/routes.py index 53010f75a7..bcd541716c 100644 --- a/src/palace/manager/api/routes.py +++ b/src/palace/manager/api/routes.py @@ -1,4 +1,6 @@ +from collections.abc import Callable from functools import update_wrapper, wraps +from typing import Any, ParamSpec, TypeVar import flask from flask import Response, make_response, request @@ -14,6 +16,10 @@ from palace.manager.sqlalchemy.hassessioncache import HasSessionCache from palace.manager.util.problem_detail import ProblemDetail +_P = ParamSpec("_P") +_T = TypeVar("_T") +_F = TypeVar("_F", bound=Callable[..., Any]) + @app.before_request def before_request(): @@ -105,13 +111,17 @@ def wrapped_function(*args, **kwargs): return update_wrapper(wrapped_function, f) -def has_library(f): +def has_library( + f: Callable[_P, _T], +) -> Callable[_P, _T | tuple[str, int, dict[str, str]]]: """Decorator to extract the library short name from the arguments.""" @wraps(f) - def decorated(*args, **kwargs): + def decorated( + *args: _P.args, **kwargs: _P.kwargs + ) -> _T | tuple[str, int, dict[str, str]]: if "library_short_name" in kwargs: - library_short_name = kwargs.pop("library_short_name") + library_short_name: str | None = kwargs.pop("library_short_name") # type: ignore[assignment] else: library_short_name = None library = app.manager.index_controller.library_for_request(library_short_name) @@ -123,15 +133,19 @@ def decorated(*args, **kwargs): return decorated -def allows_library(f): +def allows_library( + f: Callable[_P, _T], +) -> Callable[_P, _T | tuple[str, int, dict[str, str]]]: """Decorator similar to @has_library but if there is no library short name, then don't set the request library. """ @wraps(f) - def decorated(*args, **kwargs): + def decorated( + *args: _P.args, **kwargs: _P.kwargs + ) -> _T | tuple[str, int, dict[str, str]]: if "library_short_name" in kwargs: - library_short_name = kwargs.pop("library_short_name") + library_short_name: str | None = kwargs.pop("library_short_name") # type: ignore[assignment] library = app.manager.index_controller.library_for_request( library_short_name ) @@ -145,13 +159,13 @@ def decorated(*args, **kwargs): return decorated -def library_route(path, *args, **kwargs): +def library_route(path: str, *args: Any, **kwargs: Any) -> Callable[[_F], _F]: """Decorator to creates routes that have a library short name in either a subdomain or a url path prefix. If not used with @has_library, the view function must have a library_short_name argument. """ - def decorator(f): + def decorator(f: _F) -> _F: # This sets up routes for both the subdomain and the url path prefix. # The order of these determines which one will be used by url_for - # in this case it's the prefix route. diff --git a/src/palace/manager/core/app_server.py b/src/palace/manager/core/app_server.py index 443d3aa952..9598c630e4 100644 --- a/src/palace/manager/core/app_server.py +++ b/src/palace/manager/core/app_server.py @@ -6,7 +6,7 @@ from collections.abc import Callable from functools import wraps from io import BytesIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ParamSpec, TypeVar import flask from flask import Response, make_response, url_for @@ -86,9 +86,17 @@ def load_pagination_from_request( return base_class.from_request(get_arg, default_size, **kwargs) -def returns_problem_detail(f): +_P = ParamSpec("_P") +_T = TypeVar("_T") + + +def returns_problem_detail( + f: Callable[_P, _T], +) -> Callable[_P, _T | tuple[str, int, dict[str, str]]]: @wraps(f) - def decorated(*args, **kwargs): + def decorated( + *args: _P.args, **kwargs: _P.kwargs + ) -> _T | tuple[str, int, dict[str, str]]: v = f(*args, **kwargs) if isinstance(v, ProblemDetail): return v.response From a46f357680802ef49fc78e67aeafe6f707368520 Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Mon, 9 Mar 2026 12:51:13 -0700 Subject: [PATCH 4/5] Improve type hints. --- src/palace/manager/api/admin/routes.py | 188 ++++++++++++---------- src/palace/manager/api/admin/validator.py | 6 +- 2 files changed, 109 insertions(+), 85 deletions(-) diff --git a/src/palace/manager/api/admin/routes.py b/src/palace/manager/api/admin/routes.py index 3395598d04..abffacc82f 100644 --- a/src/palace/manager/api/admin/routes.py +++ b/src/palace/manager/api/admin/routes.py @@ -1,10 +1,11 @@ -from collections.abc import Callable +from collections.abc import Callable, Mapping from datetime import timedelta from functools import wraps from typing import Any, ParamSpec, TypeVar import flask from flask import Response, make_response, redirect, request, url_for +from werkzeug.wrappers import Response as WerkzeugResponse from palace.manager.api.admin.config import ( Configuration as AdminClientConfig, @@ -24,6 +25,13 @@ from palace.manager.sqlalchemy.model.admin import Admin from palace.manager.util.problem_detail import BaseProblemDetailException, ProblemDetail +# Type aliases for route return values (before decorator transformation). +# Use WerkzeugResponse as base since controllers may return redirect() etc. +ProblemDetailOrResponse = WerkzeugResponse | ProblemDetail +JsonOrProblemDetailOrResponse = ( + dict[str, Any] | Mapping[str, Any] | WerkzeugResponse | ProblemDetail | None +) + # An admin's session will expire after this amount of time and # the admin will have to log in again. app.permanent_session_lifetime = timedelta(hours=9) @@ -130,43 +138,45 @@ def decorated( return decorated -@app.route("/admin/sign_in_with_password", methods=["POST"]) +@app.route("/admin/sign_in_with_password", methods=["POST"]) # type: ignore[type-var] @returns_problem_detail -def password_auth() -> Any: +def password_auth() -> ProblemDetailOrResponse: return app.manager.admin_sign_in_controller.password_sign_in() -@app.route("/admin/sign_in") +@app.route("/admin/sign_in") # type: ignore[type-var] @returns_problem_detail -def admin_sign_in() -> Any: +def admin_sign_in() -> ProblemDetailOrResponse: return app.manager.admin_sign_in_controller.sign_in() -@app.route("/admin/sign_out") +@app.route("/admin/sign_out") # type: ignore[type-var] @returns_problem_detail @requires_admin -def admin_sign_out() -> Any: +def admin_sign_out() -> ProblemDetailOrResponse: return app.manager.admin_sign_in_controller.sign_out() -@app.route("/admin/change_password", methods=["POST"]) +@app.route("/admin/change_password", methods=["POST"]) # type: ignore[type-var] @returns_problem_detail @requires_admin -def admin_change_password() -> Any: +def admin_change_password() -> ProblemDetailOrResponse: return app.manager.admin_sign_in_controller.change_password() -@app.route("/admin/forgot_password", methods=["GET", "POST"]) +@app.route("/admin/forgot_password", methods=["GET", "POST"]) # type: ignore[type-var] @returns_problem_detail -def admin_forgot_password() -> Any: +def admin_forgot_password() -> ProblemDetailOrResponse: return app.manager.admin_reset_password_controller.forgot_password() -@app.route( +@app.route( # type: ignore[type-var] "/admin/reset_password//", methods=["GET", "POST"] ) @returns_problem_detail -def admin_reset_password(reset_password_token: str, admin_id: str) -> Any: +def admin_reset_password( + reset_password_token: str, admin_id: str +) -> ProblemDetailOrResponse | None: return app.manager.admin_reset_password_controller.reset_password( reset_password_token, int(admin_id) if admin_id.isdigit() else 0 ) @@ -176,7 +186,7 @@ def admin_reset_password(reset_password_token: str, admin_id: str) -> Any: @has_library @returns_problem_detail @requires_admin -def work_details(identifier_type: str, identifier: str) -> Any: +def work_details(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.details(identifier_type, identifier) @@ -186,7 +196,9 @@ def work_details(identifier_type: str, identifier: str) -> Any: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def work_classifications(identifier_type: str, identifier: str) -> Any: +def work_classifications( + identifier_type: str, identifier: str +) -> JsonOrProblemDetailOrResponse: return app.manager.admin_work_controller.classifications( identifier_type, identifier ) @@ -199,7 +211,9 @@ def work_classifications(identifier_type: str, identifier: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def work_custom_lists(identifier_type: str, identifier: str) -> Any: +def work_custom_lists( + identifier_type: str, identifier: str +) -> JsonOrProblemDetailOrResponse: return app.manager.admin_work_controller.custom_lists(identifier_type, identifier) @@ -210,7 +224,7 @@ def work_custom_lists(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def edit(identifier_type: str, identifier: str) -> Any: +def edit(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.edit(identifier_type, identifier) @@ -221,7 +235,9 @@ def edit(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def suppress_for_library(identifier_type: str, identifier: str) -> Any: +def suppress_for_library( + identifier_type: str, identifier: str +) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.suppress(identifier_type, identifier) @@ -232,7 +248,9 @@ def suppress_for_library(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def unsuppress_for_library(identifier_type: str, identifier: str) -> Any: +def unsuppress_for_library( + identifier_type: str, identifier: str +) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.unsuppress(identifier_type, identifier) @@ -244,7 +262,9 @@ def unsuppress_for_library(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def suppress_deprecated(identifier_type: str, identifier: str) -> Any: +def suppress_deprecated( + identifier_type: str, identifier: str +) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.suppress(identifier_type, identifier) @@ -256,7 +276,9 @@ def suppress_deprecated(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def unsuppress_deprecated(identifier_type: str, identifier: str) -> Any: +def unsuppress_deprecated( + identifier_type: str, identifier: str +) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.unsuppress(identifier_type, identifier) @@ -265,7 +287,7 @@ def unsuppress_deprecated(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def refresh(identifier_type: str, identifier: str) -> Any: +def refresh(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.refresh_metadata( identifier_type, identifier ) @@ -279,7 +301,9 @@ def refresh(identifier_type: str, identifier: str) -> Any: @returns_problem_detail @requires_admin @requires_csrf_token -def edit_classifications(identifier_type: str, identifier: str) -> Any: +def edit_classifications( + identifier_type: str, identifier: str +) -> ProblemDetailOrResponse: return app.manager.admin_work_controller.edit_classifications( identifier_type, identifier ) @@ -287,25 +311,25 @@ def edit_classifications(identifier_type: str, identifier: str) -> Any: @app.route("/admin/roles") @returns_json_or_response_or_problem_detail -def roles() -> Any: +def roles() -> JsonOrProblemDetailOrResponse: return app.manager.admin_work_controller.roles() @app.route("/admin/languages") @returns_json_or_response_or_problem_detail -def languages() -> Any: +def languages() -> JsonOrProblemDetailOrResponse: return app.manager.admin_work_controller.languages() @app.route("/admin/media") @returns_json_or_response_or_problem_detail -def media() -> Any: +def media() -> JsonOrProblemDetailOrResponse: return app.manager.admin_work_controller.media() @app.route("/admin/rights_status") @returns_json_or_response_or_problem_detail -def rights_status() -> Any: +def rights_status() -> JsonOrProblemDetailOrResponse: return app.manager.admin_work_controller.rights_status() @@ -313,7 +337,7 @@ def rights_status() -> Any: @has_library @returns_problem_detail @requires_admin -def suppressed() -> Any: +def suppressed() -> ProblemDetailOrResponse: """Returns a feed of suppressed works.""" return app.manager.admin_feed_controller.suppressed() @@ -322,7 +346,7 @@ def suppressed() -> Any: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def suppressed_search() -> Any: +def suppressed_search() -> JsonOrProblemDetailOrResponse: """Search within suppressed/hidden works.""" return app.manager.admin_feed_controller.suppressed_search() @@ -330,7 +354,7 @@ def suppressed_search() -> Any: @app.route("/admin/genres") @returns_json_or_response_or_problem_detail @requires_admin -def genres() -> Any: +def genres() -> JsonOrProblemDetailOrResponse: """Returns a JSON representation of complete genre tree.""" return app.manager.admin_feed_controller.genres() @@ -339,7 +363,7 @@ def genres() -> Any: @returns_problem_detail @allows_library @requires_admin -def bulk_circulation_events() -> Any: +def bulk_circulation_events() -> ProblemDetailOrResponse: """Returns a CSV representation of all circulation events with optional start and end times.""" ( @@ -367,7 +391,7 @@ def bulk_circulation_events() -> Any: @app.route("/admin/stats") @returns_json_or_response_or_problem_detail @requires_admin -def stats() -> Any: +def stats() -> dict[str, Any] | Mapping[str, Any]: statistics_response: StatisticsResponse = ( app.manager.admin_dashboard_controller.stats(stats_function=generate_statistics) ) @@ -377,7 +401,7 @@ def stats() -> Any: @app.route("/admin/quicksight_embed/") @returns_json_or_response_or_problem_detail @requires_admin -def generate_quicksight_url(dashboard_name: str) -> Any: +def generate_quicksight_url(dashboard_name: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_quicksight_controller.generate_quicksight_url( dashboard_name ) @@ -386,7 +410,7 @@ def generate_quicksight_url(dashboard_name: str) -> Any: @app.route("/admin/quicksight_embed/names") @returns_json_or_response_or_problem_detail @requires_admin -def get_quicksight_names() -> Any: +def get_quicksight_names() -> JsonOrProblemDetailOrResponse: return app.manager.admin_quicksight_controller.get_dashboard_names() @@ -394,7 +418,7 @@ def get_quicksight_names() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def libraries() -> Any: +def libraries() -> JsonOrProblemDetailOrResponse: return app.manager.admin_library_settings_controller.process_libraries() @@ -402,7 +426,7 @@ def libraries() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def library(library_uuid: str) -> Any: +def library(library_uuid: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_library_settings_controller.process_delete(library_uuid) @@ -410,7 +434,7 @@ def library(library_uuid: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collections() -> Any: +def collections() -> JsonOrProblemDetailOrResponse: return app.manager.admin_collection_settings_controller.process_collections() @@ -418,7 +442,7 @@ def collections() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection(collection_id: str) -> Any: +def collection(collection_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_collection_settings_controller.process_delete( collection_id ) @@ -428,7 +452,7 @@ def collection(collection_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection_import(collection_id: str) -> Any: +def collection_import(collection_id: str) -> ProblemDetailOrResponse: try: integration_id = int(collection_id) except ValueError: @@ -443,7 +467,7 @@ def collection_import(collection_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection_self_tests(identifier: str) -> Any: +def collection_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: return ( app.manager.admin_collection_settings_controller.process_collection_self_tests( identifier @@ -456,7 +480,7 @@ def collection_self_tests(identifier: str) -> Any: @allows_admin_auth_setup @requires_admin @requires_csrf_token -def individual_admins() -> Any: +def individual_admins() -> JsonOrProblemDetailOrResponse: return ( app.manager.admin_individual_admin_settings_controller.process_individual_admins() ) @@ -466,7 +490,7 @@ def individual_admins() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def individual_admin(email: str) -> Any: +def individual_admin(email: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_individual_admin_settings_controller.process_delete(email) @@ -474,7 +498,7 @@ def individual_admin(email: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_services() -> Any: +def patron_auth_services() -> JsonOrProblemDetailOrResponse: return ( app.manager.admin_patron_auth_services_controller.process_patron_auth_services() ) @@ -484,7 +508,7 @@ def patron_auth_services() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_service(service_id: str) -> Any: +def patron_auth_service(service_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_patron_auth_services_controller.process_delete(service_id) @@ -494,7 +518,7 @@ def patron_auth_service(service_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_self_tests(identifier: str) -> Any: +def patron_auth_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_patron_auth_services_controller.process_patron_auth_service_self_tests( identifier ) @@ -505,7 +529,7 @@ def patron_auth_self_tests(identifier: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lookup_patron() -> Any: +def lookup_patron() -> JsonOrProblemDetailOrResponse: return app.manager.admin_patron_controller.lookup_patron() @@ -514,7 +538,7 @@ def lookup_patron() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def reset_adobe_id() -> Any: +def reset_adobe_id() -> JsonOrProblemDetailOrResponse: return app.manager.admin_patron_controller.reset_adobe_id() @@ -522,7 +546,7 @@ def reset_adobe_id() -> Any: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def patron_auth_methods() -> Any: +def patron_auth_methods() -> JsonOrProblemDetailOrResponse: return app.manager.admin_patron_controller.get_auth_methods() @@ -531,7 +555,7 @@ def patron_auth_methods() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_debug_auth() -> Any: +def patron_debug_auth() -> JsonOrProblemDetailOrResponse: return app.manager.admin_patron_controller.debug_auth() @@ -539,7 +563,7 @@ def patron_debug_auth() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_services() -> Any: +def metadata_services() -> JsonOrProblemDetailOrResponse: return app.manager.admin_metadata_services_controller.process_metadata_services() @@ -547,7 +571,7 @@ def metadata_services() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_service(service_id: str) -> Any: +def metadata_service(service_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_metadata_services_controller.process_delete(service_id) @@ -555,7 +579,7 @@ def metadata_service(service_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_service_self_tests(identifier: str) -> Any: +def metadata_service_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_metadata_services_controller.process_metadata_service_self_tests( identifier ) @@ -565,7 +589,7 @@ def metadata_service_self_tests(identifier: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def catalog_services() -> Any: +def catalog_services() -> JsonOrProblemDetailOrResponse: return app.manager.admin_catalog_services_controller.process_catalog_services() @@ -573,7 +597,7 @@ def catalog_services() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def catalog_service(service_id: str) -> Any: +def catalog_service(service_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_catalog_services_controller.process_delete(service_id) @@ -581,7 +605,7 @@ def catalog_service(service_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_services() -> Any: +def discovery_services() -> JsonOrProblemDetailOrResponse: return app.manager.admin_discovery_services_controller.process_discovery_services() @@ -589,7 +613,7 @@ def discovery_services() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_service(service_id: str) -> Any: +def discovery_service(service_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_discovery_services_controller.process_delete(service_id) @@ -597,7 +621,7 @@ def discovery_service(service_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def announcements_for_all() -> Any: +def announcements_for_all() -> JsonOrProblemDetailOrResponse: return app.manager.admin_announcement_service.process_many() @@ -605,7 +629,7 @@ def announcements_for_all() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_service_library_registrations() -> Any: +def discovery_service_library_registrations() -> JsonOrProblemDetailOrResponse: return ( app.manager.admin_discovery_service_library_registrations_controller.process_discovery_service_library_registrations() ) @@ -616,7 +640,7 @@ def discovery_service_library_registrations() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_lists_post() -> Any: +def custom_lists_post() -> JsonOrProblemDetailOrResponse: return app.manager.admin_custom_lists_controller.custom_lists() @@ -625,7 +649,7 @@ def custom_lists_post() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_lists_get() -> Any: +def custom_lists_get() -> JsonOrProblemDetailOrResponse: return app.manager.admin_custom_lists_controller.custom_lists() @@ -634,7 +658,7 @@ def custom_lists_get() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_get(list_id: str) -> Any: +def custom_list_get(list_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_custom_lists_controller.custom_list(list_id) @@ -643,7 +667,7 @@ def custom_list_get(list_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_post(list_id: str) -> Any: +def custom_list_post(list_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_custom_lists_controller.custom_list(list_id) @@ -652,7 +676,7 @@ def custom_list_post(list_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_delete(list_id: str) -> Any: +def custom_list_delete(list_id: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_custom_lists_controller.custom_list(list_id) @@ -661,7 +685,7 @@ def custom_list_delete(list_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_share(list_id: str) -> Any: +def custom_list_share(list_id: str) -> JsonOrProblemDetailOrResponse: """Share a custom list with all libraries in the CM that share the collections of this library and works of this list""" return app.manager.admin_custom_lists_controller.share_locally(list_id) @@ -671,7 +695,7 @@ def custom_list_share(list_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_unshare(list_id: str) -> Any: +def custom_list_unshare(list_id: str) -> JsonOrProblemDetailOrResponse: """Unshare the list from all libraries, as long as no other library is using the list in its lanes""" return app.manager.admin_custom_lists_controller.share_locally(list_id) @@ -681,7 +705,7 @@ def custom_list_unshare(list_id: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lanes() -> Any: +def lanes() -> JsonOrProblemDetailOrResponse: return app.manager.admin_lanes_controller.lanes() @@ -690,7 +714,7 @@ def lanes() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane(lane_identifier: str) -> Any: +def lane(lane_identifier: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_lanes_controller.lane(lane_identifier) @@ -699,7 +723,7 @@ def lane(lane_identifier: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane_show(lane_identifier: str) -> Any: +def lane_show(lane_identifier: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_lanes_controller.show_lane(lane_identifier) @@ -708,7 +732,7 @@ def lane_show(lane_identifier: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane_hide(lane_identifier: str) -> Any: +def lane_hide(lane_identifier: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_lanes_controller.hide_lane(lane_identifier) @@ -717,7 +741,7 @@ def lane_hide(lane_identifier: str) -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def reset_lanes() -> Any: +def reset_lanes() -> JsonOrProblemDetailOrResponse: return app.manager.admin_lanes_controller.reset() @@ -726,7 +750,7 @@ def reset_lanes() -> Any: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def change_lane_order() -> Any: +def change_lane_order() -> JsonOrProblemDetailOrResponse: return app.manager.admin_lanes_controller.change_order() @@ -734,14 +758,14 @@ def change_lane_order() -> Any: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def search_field_values() -> Any: +def search_field_values() -> JsonOrProblemDetailOrResponse: return app.manager.admin_search_controller.search_field_values() @app.route("/admin/diagnostics") @requires_admin @returns_json_or_response_or_problem_detail -def diagnostics() -> Any: +def diagnostics() -> JsonOrProblemDetailOrResponse: return app.manager.timestamps_controller.diagnostics() @@ -752,7 +776,7 @@ def diagnostics() -> Any: @allows_library @returns_json_or_response_or_problem_detail @requires_admin -def inventory_report_info() -> Any: +def inventory_report_info() -> JsonOrProblemDetailOrResponse: return app.manager.admin_report_controller.inventory_report_info() @@ -763,7 +787,7 @@ def inventory_report_info() -> Any: @allows_library @returns_json_or_response_or_problem_detail @requires_admin -def generate_inventory_report() -> Any: +def generate_inventory_report() -> JsonOrProblemDetailOrResponse: return app.manager.admin_report_controller.generate_inventory_report() @@ -771,12 +795,12 @@ def generate_inventory_report() -> Any: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def generate_report(report_key: str) -> Any: +def generate_report(report_key: str) -> JsonOrProblemDetailOrResponse: return app.manager.admin_report_controller.generate_report(report_key=report_key) @app.route("/admin/sign_in_again") -def admin_sign_in_again() -> Any: +def admin_sign_in_again() -> WerkzeugResponse | str: """Allows an admin with expired credentials to sign back in from a new browser tab so they won't lose changes. """ @@ -805,19 +829,19 @@ def admin_view( book: str | None = None, etc: str | None = None, **kwargs: Any, -) -> Any: +) -> WerkzeugResponse: return app.manager.admin_view_controller(collection, book, path=etc) @app.route("/admin/", strict_slashes=False) -def admin_base(**kwargs: Any) -> Any: +def admin_base(**kwargs: Any) -> WerkzeugResponse: return redirect(url_for("admin_view", _external=True)) @app.route("/admin/libraries/import", strict_slashes=False, methods=["POST"]) @returns_json_or_response_or_problem_detail @requires_basic_auth -def import_libraries() -> Any: +def import_libraries() -> JsonOrProblemDetailOrResponse: """Import multiple libraries from a list of library configurations.""" return app.manager.admin_library_settings_controller.import_libraries() @@ -825,9 +849,9 @@ def import_libraries() -> Any: # This path is used only in debug mode to serve frontend assets. if AdminClientConfig.operational_mode() == OperationalMode.development: - @app.route("/admin/static/") + @app.route("/admin/static/") # type: ignore[type-var] @returns_problem_detail - def admin_static_file(filename: str) -> Any: + def admin_static_file(filename: str) -> ProblemDetailOrResponse: return StaticFileController.static_file( AdminClientConfig.static_files_directory(), filename ) diff --git a/src/palace/manager/api/admin/validator.py b/src/palace/manager/api/admin/validator.py index d62c969bb2..180492d850 100644 --- a/src/palace/manager/api/admin/validator.py +++ b/src/palace/manager/api/admin/validator.py @@ -35,7 +35,7 @@ def _extract_inputs( self, settings: list[dict[str, Any]], value: str, - form: dict[str, Any] | None, + form: dict[str, Any], key: str = "format", is_list: bool = False, should_zip: bool = False, @@ -98,7 +98,7 @@ def validate_url( return None # Find the fields that have to do with URLs and are not blank. url_inputs = self._extract_inputs( - settings, "url", content.get("form"), should_zip=True + settings, "url", content["form"], should_zip=True ) for field, urls in url_inputs: @@ -132,7 +132,7 @@ def validate_number( return None # Find the fields that should have numeric input and are not blank. number_inputs = self._extract_inputs( - settings, "number", content.get("form"), key="type", should_zip=True + settings, "number", content["form"], key="type", should_zip=True ) for field, number in number_inputs: error = self._number_error(field, number) From 7bcdbc967f1ac5d88045d03e6918b7e8e65f5e59 Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Tue, 10 Mar 2026 10:18:27 -0700 Subject: [PATCH 5/5] Provide specific (ie not Any) return types in src/palace/manager/api/admin/routes.py. --- src/palace/manager/api/admin/routes.py | 184 ++++++++++++------------- 1 file changed, 91 insertions(+), 93 deletions(-) diff --git a/src/palace/manager/api/admin/routes.py b/src/palace/manager/api/admin/routes.py index abffacc82f..de986c9abd 100644 --- a/src/palace/manager/api/admin/routes.py +++ b/src/palace/manager/api/admin/routes.py @@ -23,15 +23,9 @@ from palace.manager.core.app_server import returns_problem_detail from palace.manager.core.problem_details import INVALID_INPUT from palace.manager.sqlalchemy.model.admin import Admin +from palace.manager.sqlalchemy.model.contributor import Contributor from palace.manager.util.problem_detail import BaseProblemDetailException, ProblemDetail -# Type aliases for route return values (before decorator transformation). -# Use WerkzeugResponse as base since controllers may return redirect() etc. -ProblemDetailOrResponse = WerkzeugResponse | ProblemDetail -JsonOrProblemDetailOrResponse = ( - dict[str, Any] | Mapping[str, Any] | WerkzeugResponse | ProblemDetail | None -) - # An admin's session will expire after this amount of time and # the admin will have to log in again. app.permanent_session_lifetime = timedelta(hours=9) @@ -138,45 +132,45 @@ def decorated( return decorated -@app.route("/admin/sign_in_with_password", methods=["POST"]) # type: ignore[type-var] -@returns_problem_detail -def password_auth() -> ProblemDetailOrResponse: +@app.route("/admin/sign_in_with_password", methods=["POST"]) +@returns_json_or_response_or_problem_detail +def password_auth() -> WerkzeugResponse | ProblemDetail: return app.manager.admin_sign_in_controller.password_sign_in() -@app.route("/admin/sign_in") # type: ignore[type-var] -@returns_problem_detail -def admin_sign_in() -> ProblemDetailOrResponse: +@app.route("/admin/sign_in") +@returns_json_or_response_or_problem_detail +def admin_sign_in() -> WerkzeugResponse | ProblemDetail: return app.manager.admin_sign_in_controller.sign_in() -@app.route("/admin/sign_out") # type: ignore[type-var] -@returns_problem_detail +@app.route("/admin/sign_out") +@returns_json_or_response_or_problem_detail @requires_admin -def admin_sign_out() -> ProblemDetailOrResponse: +def admin_sign_out() -> WerkzeugResponse: return app.manager.admin_sign_in_controller.sign_out() -@app.route("/admin/change_password", methods=["POST"]) # type: ignore[type-var] -@returns_problem_detail +@app.route("/admin/change_password", methods=["POST"]) +@returns_json_or_response_or_problem_detail @requires_admin -def admin_change_password() -> ProblemDetailOrResponse: +def admin_change_password() -> Response: return app.manager.admin_sign_in_controller.change_password() -@app.route("/admin/forgot_password", methods=["GET", "POST"]) # type: ignore[type-var] -@returns_problem_detail -def admin_forgot_password() -> ProblemDetailOrResponse: +@app.route("/admin/forgot_password", methods=["GET", "POST"]) +@returns_json_or_response_or_problem_detail +def admin_forgot_password() -> WerkzeugResponse | ProblemDetail: return app.manager.admin_reset_password_controller.forgot_password() -@app.route( # type: ignore[type-var] +@app.route( "/admin/reset_password//", methods=["GET", "POST"] ) -@returns_problem_detail +@returns_json_or_response_or_problem_detail def admin_reset_password( reset_password_token: str, admin_id: str -) -> ProblemDetailOrResponse | None: +) -> WerkzeugResponse | None: return app.manager.admin_reset_password_controller.reset_password( reset_password_token, int(admin_id) if admin_id.isdigit() else 0 ) @@ -186,7 +180,7 @@ def admin_reset_password( @has_library @returns_problem_detail @requires_admin -def work_details(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: +def work_details(identifier_type: str, identifier: str) -> Response | ProblemDetail: return app.manager.admin_work_controller.details(identifier_type, identifier) @@ -198,7 +192,7 @@ def work_details(identifier_type: str, identifier: str) -> ProblemDetailOrRespon @requires_admin def work_classifications( identifier_type: str, identifier: str -) -> JsonOrProblemDetailOrResponse: +) -> dict[str, Any] | ProblemDetail: return app.manager.admin_work_controller.classifications( identifier_type, identifier ) @@ -213,7 +207,7 @@ def work_classifications( @requires_csrf_token def work_custom_lists( identifier_type: str, identifier: str -) -> JsonOrProblemDetailOrResponse: +) -> Mapping[str, Any] | Response: return app.manager.admin_work_controller.custom_lists(identifier_type, identifier) @@ -224,7 +218,7 @@ def work_custom_lists( @returns_problem_detail @requires_admin @requires_csrf_token -def edit(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: +def edit(identifier_type: str, identifier: str) -> Response | ProblemDetail: return app.manager.admin_work_controller.edit(identifier_type, identifier) @@ -237,7 +231,7 @@ def edit(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: @requires_csrf_token def suppress_for_library( identifier_type: str, identifier: str -) -> ProblemDetailOrResponse: +) -> Response | ProblemDetail: return app.manager.admin_work_controller.suppress(identifier_type, identifier) @@ -250,7 +244,7 @@ def suppress_for_library( @requires_csrf_token def unsuppress_for_library( identifier_type: str, identifier: str -) -> ProblemDetailOrResponse: +) -> Response | ProblemDetail: return app.manager.admin_work_controller.unsuppress(identifier_type, identifier) @@ -264,7 +258,7 @@ def unsuppress_for_library( @requires_csrf_token def suppress_deprecated( identifier_type: str, identifier: str -) -> ProblemDetailOrResponse: +) -> Response | ProblemDetail: return app.manager.admin_work_controller.suppress(identifier_type, identifier) @@ -278,7 +272,7 @@ def suppress_deprecated( @requires_csrf_token def unsuppress_deprecated( identifier_type: str, identifier: str -) -> ProblemDetailOrResponse: +) -> Response | ProblemDetail: return app.manager.admin_work_controller.unsuppress(identifier_type, identifier) @@ -287,7 +281,7 @@ def unsuppress_deprecated( @returns_problem_detail @requires_admin @requires_csrf_token -def refresh(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: +def refresh(identifier_type: str, identifier: str) -> Response | ProblemDetail: return app.manager.admin_work_controller.refresh_metadata( identifier_type, identifier ) @@ -303,7 +297,7 @@ def refresh(identifier_type: str, identifier: str) -> ProblemDetailOrResponse: @requires_csrf_token def edit_classifications( identifier_type: str, identifier: str -) -> ProblemDetailOrResponse: +) -> Response | ProblemDetail: return app.manager.admin_work_controller.edit_classifications( identifier_type, identifier ) @@ -311,25 +305,25 @@ def edit_classifications( @app.route("/admin/roles") @returns_json_or_response_or_problem_detail -def roles() -> JsonOrProblemDetailOrResponse: +def roles() -> dict[str, Contributor.Role]: return app.manager.admin_work_controller.roles() @app.route("/admin/languages") @returns_json_or_response_or_problem_detail -def languages() -> JsonOrProblemDetailOrResponse: +def languages() -> dict[str, list[str]]: return app.manager.admin_work_controller.languages() @app.route("/admin/media") @returns_json_or_response_or_problem_detail -def media() -> JsonOrProblemDetailOrResponse: +def media() -> dict[str, str]: return app.manager.admin_work_controller.media() @app.route("/admin/rights_status") @returns_json_or_response_or_problem_detail -def rights_status() -> JsonOrProblemDetailOrResponse: +def rights_status() -> dict[str, dict[str, str | bool]]: return app.manager.admin_work_controller.rights_status() @@ -337,7 +331,7 @@ def rights_status() -> JsonOrProblemDetailOrResponse: @has_library @returns_problem_detail @requires_admin -def suppressed() -> ProblemDetailOrResponse: +def suppressed() -> Response | ProblemDetail: """Returns a feed of suppressed works.""" return app.manager.admin_feed_controller.suppressed() @@ -346,7 +340,7 @@ def suppressed() -> ProblemDetailOrResponse: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def suppressed_search() -> JsonOrProblemDetailOrResponse: +def suppressed_search() -> Response | ProblemDetail: """Search within suppressed/hidden works.""" return app.manager.admin_feed_controller.suppressed_search() @@ -354,7 +348,7 @@ def suppressed_search() -> JsonOrProblemDetailOrResponse: @app.route("/admin/genres") @returns_json_or_response_or_problem_detail @requires_admin -def genres() -> JsonOrProblemDetailOrResponse: +def genres() -> dict[str, dict[str, dict[str, str | list[str]]]]: """Returns a JSON representation of complete genre tree.""" return app.manager.admin_feed_controller.genres() @@ -363,7 +357,7 @@ def genres() -> JsonOrProblemDetailOrResponse: @returns_problem_detail @allows_library @requires_admin -def bulk_circulation_events() -> ProblemDetailOrResponse: +def bulk_circulation_events() -> Response | ProblemDetail: """Returns a CSV representation of all circulation events with optional start and end times.""" ( @@ -391,7 +385,7 @@ def bulk_circulation_events() -> ProblemDetailOrResponse: @app.route("/admin/stats") @returns_json_or_response_or_problem_detail @requires_admin -def stats() -> dict[str, Any] | Mapping[str, Any]: +def stats() -> dict[str, Any]: statistics_response: StatisticsResponse = ( app.manager.admin_dashboard_controller.stats(stats_function=generate_statistics) ) @@ -401,7 +395,7 @@ def stats() -> dict[str, Any] | Mapping[str, Any]: @app.route("/admin/quicksight_embed/") @returns_json_or_response_or_problem_detail @requires_admin -def generate_quicksight_url(dashboard_name: str) -> JsonOrProblemDetailOrResponse: +def generate_quicksight_url(dashboard_name: str) -> dict[str, str]: return app.manager.admin_quicksight_controller.generate_quicksight_url( dashboard_name ) @@ -410,7 +404,7 @@ def generate_quicksight_url(dashboard_name: str) -> JsonOrProblemDetailOrRespons @app.route("/admin/quicksight_embed/names") @returns_json_or_response_or_problem_detail @requires_admin -def get_quicksight_names() -> JsonOrProblemDetailOrResponse: +def get_quicksight_names() -> dict[str, list[str]]: return app.manager.admin_quicksight_controller.get_dashboard_names() @@ -418,7 +412,7 @@ def get_quicksight_names() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def libraries() -> JsonOrProblemDetailOrResponse: +def libraries() -> Response | ProblemDetail: return app.manager.admin_library_settings_controller.process_libraries() @@ -426,7 +420,7 @@ def libraries() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def library(library_uuid: str) -> JsonOrProblemDetailOrResponse: +def library(library_uuid: str) -> Response: return app.manager.admin_library_settings_controller.process_delete(library_uuid) @@ -434,7 +428,7 @@ def library(library_uuid: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collections() -> JsonOrProblemDetailOrResponse: +def collections() -> Response | ProblemDetail: return app.manager.admin_collection_settings_controller.process_collections() @@ -442,7 +436,7 @@ def collections() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection(collection_id: str) -> JsonOrProblemDetailOrResponse: +def collection(collection_id: str) -> Response | ProblemDetail: return app.manager.admin_collection_settings_controller.process_delete( collection_id ) @@ -452,7 +446,7 @@ def collection(collection_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection_import(collection_id: str) -> ProblemDetailOrResponse: +def collection_import(collection_id: str) -> Response | ProblemDetail: try: integration_id = int(collection_id) except ValueError: @@ -467,7 +461,7 @@ def collection_import(collection_id: str) -> ProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def collection_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: +def collection_self_tests(identifier: str) -> Response | ProblemDetail: return ( app.manager.admin_collection_settings_controller.process_collection_self_tests( identifier @@ -480,7 +474,7 @@ def collection_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: @allows_admin_auth_setup @requires_admin @requires_csrf_token -def individual_admins() -> JsonOrProblemDetailOrResponse: +def individual_admins() -> dict[str, list[dict[str, Any]]] | Response | ProblemDetail: return ( app.manager.admin_individual_admin_settings_controller.process_individual_admins() ) @@ -490,7 +484,7 @@ def individual_admins() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def individual_admin(email: str) -> JsonOrProblemDetailOrResponse: +def individual_admin(email: str) -> Response | ProblemDetail: return app.manager.admin_individual_admin_settings_controller.process_delete(email) @@ -498,7 +492,7 @@ def individual_admin(email: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_services() -> JsonOrProblemDetailOrResponse: +def patron_auth_services() -> Response | ProblemDetail: return ( app.manager.admin_patron_auth_services_controller.process_patron_auth_services() ) @@ -508,7 +502,7 @@ def patron_auth_services() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_service(service_id: str) -> JsonOrProblemDetailOrResponse: +def patron_auth_service(service_id: str) -> Response | ProblemDetail: return app.manager.admin_patron_auth_services_controller.process_delete(service_id) @@ -518,7 +512,7 @@ def patron_auth_service(service_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_auth_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: +def patron_auth_self_tests(identifier: str) -> Response | ProblemDetail: return app.manager.admin_patron_auth_services_controller.process_patron_auth_service_self_tests( identifier ) @@ -529,7 +523,7 @@ def patron_auth_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lookup_patron() -> JsonOrProblemDetailOrResponse: +def lookup_patron() -> dict[str, Any] | ProblemDetail: return app.manager.admin_patron_controller.lookup_patron() @@ -538,7 +532,7 @@ def lookup_patron() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def reset_adobe_id() -> JsonOrProblemDetailOrResponse: +def reset_adobe_id() -> Response | ProblemDetail: return app.manager.admin_patron_controller.reset_adobe_id() @@ -546,7 +540,7 @@ def reset_adobe_id() -> JsonOrProblemDetailOrResponse: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def patron_auth_methods() -> JsonOrProblemDetailOrResponse: +def patron_auth_methods() -> dict[str, Any] | ProblemDetail: return app.manager.admin_patron_controller.get_auth_methods() @@ -555,7 +549,7 @@ def patron_auth_methods() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def patron_debug_auth() -> JsonOrProblemDetailOrResponse: +def patron_debug_auth() -> dict[str, Any] | ProblemDetail: return app.manager.admin_patron_controller.debug_auth() @@ -563,7 +557,7 @@ def patron_debug_auth() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_services() -> JsonOrProblemDetailOrResponse: +def metadata_services() -> Response | ProblemDetail: return app.manager.admin_metadata_services_controller.process_metadata_services() @@ -571,7 +565,7 @@ def metadata_services() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_service(service_id: str) -> JsonOrProblemDetailOrResponse: +def metadata_service(service_id: str) -> Response | ProblemDetail: return app.manager.admin_metadata_services_controller.process_delete(service_id) @@ -579,7 +573,7 @@ def metadata_service(service_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def metadata_service_self_tests(identifier: str) -> JsonOrProblemDetailOrResponse: +def metadata_service_self_tests(identifier: str) -> Response | ProblemDetail: return app.manager.admin_metadata_services_controller.process_metadata_service_self_tests( identifier ) @@ -589,7 +583,7 @@ def metadata_service_self_tests(identifier: str) -> JsonOrProblemDetailOrRespons @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def catalog_services() -> JsonOrProblemDetailOrResponse: +def catalog_services() -> Response | ProblemDetail: return app.manager.admin_catalog_services_controller.process_catalog_services() @@ -597,7 +591,7 @@ def catalog_services() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def catalog_service(service_id: str) -> JsonOrProblemDetailOrResponse: +def catalog_service(service_id: str) -> Response | ProblemDetail: return app.manager.admin_catalog_services_controller.process_delete(service_id) @@ -605,7 +599,7 @@ def catalog_service(service_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_services() -> JsonOrProblemDetailOrResponse: +def discovery_services() -> Response | ProblemDetail: return app.manager.admin_discovery_services_controller.process_discovery_services() @@ -613,7 +607,7 @@ def discovery_services() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_service(service_id: str) -> JsonOrProblemDetailOrResponse: +def discovery_service(service_id: str) -> Response | ProblemDetail: return app.manager.admin_discovery_services_controller.process_delete(service_id) @@ -621,7 +615,7 @@ def discovery_service(service_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def announcements_for_all() -> JsonOrProblemDetailOrResponse: +def announcements_for_all() -> dict[str, Any] | ProblemDetail: return app.manager.admin_announcement_service.process_many() @@ -629,7 +623,9 @@ def announcements_for_all() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def discovery_service_library_registrations() -> JsonOrProblemDetailOrResponse: +def discovery_service_library_registrations() -> ( + Response | dict[str, Any] | ProblemDetail +): return ( app.manager.admin_discovery_service_library_registrations_controller.process_discovery_service_library_registrations() ) @@ -640,7 +636,7 @@ def discovery_service_library_registrations() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_lists_post() -> JsonOrProblemDetailOrResponse: +def custom_lists_post() -> dict[str, Any] | ProblemDetail | Response | None: return app.manager.admin_custom_lists_controller.custom_lists() @@ -649,7 +645,7 @@ def custom_lists_post() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_lists_get() -> JsonOrProblemDetailOrResponse: +def custom_lists_get() -> dict[str, Any] | ProblemDetail | Response | None: return app.manager.admin_custom_lists_controller.custom_lists() @@ -658,7 +654,7 @@ def custom_lists_get() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_get(list_id: str) -> JsonOrProblemDetailOrResponse: +def custom_list_get(list_id: str) -> Response | dict[str, Any] | ProblemDetail | None: return app.manager.admin_custom_lists_controller.custom_list(list_id) @@ -667,7 +663,7 @@ def custom_list_get(list_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_post(list_id: str) -> JsonOrProblemDetailOrResponse: +def custom_list_post(list_id: str) -> Response | dict[str, Any] | ProblemDetail | None: return app.manager.admin_custom_lists_controller.custom_list(list_id) @@ -676,7 +672,9 @@ def custom_list_post(list_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_delete(list_id: str) -> JsonOrProblemDetailOrResponse: +def custom_list_delete( + list_id: str, +) -> Response | dict[str, Any] | ProblemDetail | None: return app.manager.admin_custom_lists_controller.custom_list(list_id) @@ -685,7 +683,7 @@ def custom_list_delete(list_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_share(list_id: str) -> JsonOrProblemDetailOrResponse: +def custom_list_share(list_id: str) -> ProblemDetail | dict[str, int] | Response: """Share a custom list with all libraries in the CM that share the collections of this library and works of this list""" return app.manager.admin_custom_lists_controller.share_locally(list_id) @@ -695,7 +693,7 @@ def custom_list_share(list_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def custom_list_unshare(list_id: str) -> JsonOrProblemDetailOrResponse: +def custom_list_unshare(list_id: str) -> ProblemDetail | dict[str, int] | Response: """Unshare the list from all libraries, as long as no other library is using the list in its lanes""" return app.manager.admin_custom_lists_controller.share_locally(list_id) @@ -705,7 +703,7 @@ def custom_list_unshare(list_id: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lanes() -> JsonOrProblemDetailOrResponse: +def lanes() -> dict[str, list[dict[str, Any]]] | Response | ProblemDetail: return app.manager.admin_lanes_controller.lanes() @@ -714,7 +712,7 @@ def lanes() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane(lane_identifier: str) -> JsonOrProblemDetailOrResponse: +def lane(lane_identifier: str) -> Response | ProblemDetail: return app.manager.admin_lanes_controller.lane(lane_identifier) @@ -723,7 +721,7 @@ def lane(lane_identifier: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane_show(lane_identifier: str) -> JsonOrProblemDetailOrResponse: +def lane_show(lane_identifier: str) -> Response | ProblemDetail: return app.manager.admin_lanes_controller.show_lane(lane_identifier) @@ -732,7 +730,7 @@ def lane_show(lane_identifier: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def lane_hide(lane_identifier: str) -> JsonOrProblemDetailOrResponse: +def lane_hide(lane_identifier: str) -> Response | ProblemDetail: return app.manager.admin_lanes_controller.hide_lane(lane_identifier) @@ -741,7 +739,7 @@ def lane_hide(lane_identifier: str) -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def reset_lanes() -> JsonOrProblemDetailOrResponse: +def reset_lanes() -> Response: return app.manager.admin_lanes_controller.reset() @@ -750,7 +748,7 @@ def reset_lanes() -> JsonOrProblemDetailOrResponse: @returns_json_or_response_or_problem_detail @requires_admin @requires_csrf_token -def change_lane_order() -> JsonOrProblemDetailOrResponse: +def change_lane_order() -> Response: return app.manager.admin_lanes_controller.change_order() @@ -758,14 +756,14 @@ def change_lane_order() -> JsonOrProblemDetailOrResponse: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def search_field_values() -> JsonOrProblemDetailOrResponse: +def search_field_values() -> dict[str, dict[str, int]]: return app.manager.admin_search_controller.search_field_values() @app.route("/admin/diagnostics") @requires_admin @returns_json_or_response_or_problem_detail -def diagnostics() -> JsonOrProblemDetailOrResponse: +def diagnostics() -> dict[str, dict[str, dict[str, list[dict[str, Any]]]]]: return app.manager.timestamps_controller.diagnostics() @@ -776,7 +774,7 @@ def diagnostics() -> JsonOrProblemDetailOrResponse: @allows_library @returns_json_or_response_or_problem_detail @requires_admin -def inventory_report_info() -> JsonOrProblemDetailOrResponse: +def inventory_report_info() -> Response: return app.manager.admin_report_controller.inventory_report_info() @@ -787,7 +785,7 @@ def inventory_report_info() -> JsonOrProblemDetailOrResponse: @allows_library @returns_json_or_response_or_problem_detail @requires_admin -def generate_inventory_report() -> JsonOrProblemDetailOrResponse: +def generate_inventory_report() -> Response | ProblemDetail: return app.manager.admin_report_controller.generate_inventory_report() @@ -795,7 +793,7 @@ def generate_inventory_report() -> JsonOrProblemDetailOrResponse: @has_library @returns_json_or_response_or_problem_detail @requires_admin -def generate_report(report_key: str) -> JsonOrProblemDetailOrResponse: +def generate_report(report_key: str) -> Response: return app.manager.admin_report_controller.generate_report(report_key=report_key) @@ -828,20 +826,20 @@ def admin_view( collection: str | None = None, book: str | None = None, etc: str | None = None, - **kwargs: Any, + **kwargs: str, ) -> WerkzeugResponse: return app.manager.admin_view_controller(collection, book, path=etc) @app.route("/admin/", strict_slashes=False) -def admin_base(**kwargs: Any) -> WerkzeugResponse: +def admin_base(**kwargs: str) -> WerkzeugResponse: return redirect(url_for("admin_view", _external=True)) @app.route("/admin/libraries/import", strict_slashes=False, methods=["POST"]) @returns_json_or_response_or_problem_detail @requires_basic_auth -def import_libraries() -> JsonOrProblemDetailOrResponse: +def import_libraries() -> Response | ProblemDetail: """Import multiple libraries from a list of library configurations.""" return app.manager.admin_library_settings_controller.import_libraries() @@ -849,9 +847,9 @@ def import_libraries() -> JsonOrProblemDetailOrResponse: # This path is used only in debug mode to serve frontend assets. if AdminClientConfig.operational_mode() == OperationalMode.development: - @app.route("/admin/static/") # type: ignore[type-var] + @app.route("/admin/static/") @returns_problem_detail - def admin_static_file(filename: str) -> ProblemDetailOrResponse: + def admin_static_file(filename: str) -> Response: return StaticFileController.static_file( AdminClientConfig.static_files_directory(), filename )