diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc48778ae..01696f186 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.272 + rev: v0.0.277 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -27,7 +27,7 @@ repos: args: [-l, '79', -t, py311] - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.43.0 + rev: v8.44.0 hooks: - id: eslint additional_dependencies: diff --git a/src/gafaelfawr/config.py b/src/gafaelfawr/config.py index 349c0d95b..e35ea3045 100644 --- a/src/gafaelfawr/config.py +++ b/src/gafaelfawr/config.py @@ -20,7 +20,13 @@ from typing import Any, Self import yaml -from pydantic import AnyHttpUrl, IPvAnyNetwork, root_validator, validator +from pydantic import ( + AnyHttpUrl, + Field, + IPvAnyNetwork, + root_validator, + validator, +) from safir.logging import LogLevel, Profile, configure_logging from safir.pydantic import CamelCaseModel, validate_exactly_one_of @@ -76,8 +82,11 @@ class OIDCSettings(CamelCaseModel): login_url: AnyHttpUrl """URL to which to send the user to initiate authentication.""" - login_params: dict[str, str] = {} - """Additional parameters to the login URL.""" + login_params: dict[str, str] = Field( + {}, + title="Additional login parameters", + description="Additional parameters to the login URL", + ) redirect_url: AnyHttpUrl """Return URL to which the authentication provider should send the user. @@ -96,12 +105,15 @@ class OIDCSettings(CamelCaseModel): URL so that they can register. """ - scopes: list[str] = [] - """Scopes to request from the authentication provider. - - The ``openid`` scope will always be added and does not need to be - specified. - """ + scopes: list[str] = Field( + [], + title="Scopes to request", + description=( + "Scopes to request from the authentication provider. The" + " `openid` scope will always be added and does not need to be" + " specified." + ), + ) issuer: str """Expected issuer of the ID token.""" @@ -283,8 +295,13 @@ class QuotaGrantSettings(CamelCaseModel): overall quota configuration. """ - api: dict[str, int] = {} - """Mapping of service names to quota of requests per 15 minutes.""" + api: dict[str, int] = Field( + {}, + title="Service quotas", + description=( + "Mapping of service names to quota of requests per 15 minutes" + ), + ) notebook: NotebookQuotaSettings | None = None """Quota settings for the Notebook Aspect.""" @@ -296,8 +313,11 @@ class QuotaSettings(CamelCaseModel): default: QuotaGrantSettings """Default quotas for all users.""" - groups: dict[str, QuotaGrantSettings] = {} - """Additional quota grants by group name.""" + groups: dict[str, QuotaGrantSettings] = Field( + {}, + title="Quota grants by group", + description="Additional quota grants by group name", + ) class Settings(CamelCaseModel): @@ -391,11 +411,19 @@ class Settings(CamelCaseModel): initial_admins: list[str] """Initial token administrators to configure when initializing database.""" - known_scopes: dict[str, str] = {} - """Known scopes (the keys) and their descriptions (the values).""" + known_scopes: dict[str, str] = Field( + {}, + title="Known scopes", + description=( + "Known scopes (the keys) and their descriptions (the values)" + ), + ) - group_mapping: dict[str, list[str]] = {} - """Mappings of scopes to lists of groups that provide them.""" + group_mapping: dict[str, list[str]] = Field( + {}, + title="Scope to group mapping", + description="Mappings of scopes to lists of groups that provide them", + ) @validator("initial_admins", each_item=True) def _validate_initial_admins(cls, v: str) -> str: diff --git a/src/gafaelfawr/handlers/oidc.py b/src/gafaelfawr/handlers/oidc.py index 8b7929cb0..37b54a8b0 100644 --- a/src/gafaelfawr/handlers/oidc.py +++ b/src/gafaelfawr/handlers/oidc.py @@ -120,13 +120,12 @@ async def get_login( if error: e = InvalidRequestError(error) context.logger.warning("%s", e.message, error=str(e)) - return_url = build_return_url( + return build_return_url( parsed_redirect_uri, state=state, error=e.error, error_description=str(e), ) - return return_url # Get an authorization code and return it. code = await oidc_service.issue_code( diff --git a/src/gafaelfawr/models/admin.py b/src/gafaelfawr/models/admin.py index ec037abd8..36c9a8560 100644 --- a/src/gafaelfawr/models/admin.py +++ b/src/gafaelfawr/models/admin.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import ClassVar + from pydantic import BaseModel __all__ = ["Admin"] @@ -15,4 +17,6 @@ class Admin(BaseModel): class Config: orm_mode = True - schema_extra = {"example": {"username": "adminuser"}} + schema_extra: ClassVar[dict[str, dict[str, str]]] = { + "example": {"username": "adminuser"} + } diff --git a/src/gafaelfawr/models/history.py b/src/gafaelfawr/models/history.py index 3e43a7888..63c937fd7 100644 --- a/src/gafaelfawr/models/history.py +++ b/src/gafaelfawr/models/history.py @@ -2,10 +2,11 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import UTC, datetime from enum import Enum -from typing import Any, Generic, Self, TypeVar +from typing import Any, ClassVar, Generic, Self, TypeVar from urllib.parse import parse_qs, urlencode from pydantic import BaseModel, Field, validator @@ -353,7 +354,9 @@ class TokenChangeHistoryEntry(BaseModel): ) class Config: - json_encoders = {datetime: lambda v: int(v.timestamp())} + json_encoders: ClassVar[dict[type, Callable]] = { + datetime: lambda v: int(v.timestamp()) + } orm_mode = True _normalize_scopes = validator( diff --git a/src/gafaelfawr/models/oidc.py b/src/gafaelfawr/models/oidc.py index 74ae964f8..855f8fc87 100644 --- a/src/gafaelfawr/models/oidc.py +++ b/src/gafaelfawr/models/oidc.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Callable from datetime import datetime -from typing import Any, Self +from typing import Any, ClassVar, Self from pydantic import BaseModel, Field, validator from safir.datetime import current_datetime @@ -113,7 +114,9 @@ class OIDCAuthorization(BaseModel): ) class Config: - json_encoders = {datetime: lambda v: int(v.timestamp())} + json_encoders: ClassVar[dict[type, Callable]] = { + datetime: lambda v: int(v.timestamp()) + } _normalize_created_at = validator( "created_at", allow_reuse=True, pre=True diff --git a/src/gafaelfawr/models/token.py b/src/gafaelfawr/models/token.py index 620093b89..70fb7a8be 100644 --- a/src/gafaelfawr/models/token.py +++ b/src/gafaelfawr/models/token.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Callable from datetime import datetime from enum import Enum -from typing import Any, Self +from typing import Any, ClassVar, Self from pydantic import BaseModel, Field, validator from safir.datetime import current_datetime @@ -304,7 +305,9 @@ class TokenInfo(TokenBase): class Config: orm_mode = True - json_encoders = {datetime: lambda v: int(v.timestamp())} + json_encoders: ClassVar[dict[type, Callable]] = { + datetime: lambda v: int(v.timestamp()) + } _normalize_created = validator( "created", "last_used", "expires", allow_reuse=True, pre=True @@ -407,7 +410,9 @@ class TokenData(TokenBase, TokenUserInfo): token: Token = Field(..., title="Associated token") class Config: - json_encoders = {datetime: lambda v: int(v.timestamp())} + json_encoders: ClassVar[dict[type, Callable]] = { + datetime: lambda v: int(v.timestamp()) + } @classmethod def bootstrap_token(cls) -> Self: diff --git a/src/gafaelfawr/providers/github.py b/src/gafaelfawr/providers/github.py index 293fc3fdc..d50f3aa01 100644 --- a/src/gafaelfawr/providers/github.py +++ b/src/gafaelfawr/providers/github.py @@ -52,7 +52,7 @@ class GitHubProvider(Provider): _USER_URL = "https://api.github.com/user" """URL from which to request user metadata.""" - _SCOPES = ["read:org", "read:user", "user:email"] + _SCOPES = ("read:org", "read:user", "user:email") """Access scopes to request from GitHub.""" def __init__( diff --git a/src/gafaelfawr/storage/token.py b/src/gafaelfawr/storage/token.py index 1ce772f8c..596a8d4f6 100644 --- a/src/gafaelfawr/storage/token.py +++ b/src/gafaelfawr/storage/token.py @@ -523,10 +523,7 @@ async def list(self) -> list[str]: list of str The tokens found in Redis (by looking for valid keys). """ - keys = [] - async for key in self._storage.scan("*"): - keys.append(key) - return keys + return [k async for k in self._storage.scan("*")] async def store_data(self, data: TokenData) -> None: """Store the data for a token. diff --git a/tests/handlers/auth_load_test.py b/tests/handlers/auth_load_test.py index 7aad07d09..d82ab128b 100644 --- a/tests/handlers/auth_load_test.py +++ b/tests/handlers/auth_load_test.py @@ -26,13 +26,8 @@ async def test_notebook(client: AsyncClient, factory: Factory) -> None: ) await set_session_cookie(client, data.token) - request_awaits = [] - for _ in range(100): - request_awaits.append( - client.get( - "/auth", params={"scope": "exec:test", "notebook": "true"} - ) - ) + params = {"scope": "exec:test", "notebook": "true"} + request_awaits = [client.get("/auth", params=params) for _ in range(100)] responses = await asyncio.gather(*request_awaits) assert responses[0].status_code == 200 token = Token.from_str(responses[0].headers["X-Auth-Request-Token"]) @@ -48,18 +43,12 @@ async def test_internal(client: AsyncClient, factory: Factory) -> None: ) await set_session_cookie(client, data.token) - request_awaits = [] - for _ in range(100): - request_awaits.append( - client.get( - "/auth", - params={ - "scope": "exec:test", - "delegate_to": "a-service", - "delegate_scope": "read:all", - }, - ) - ) + params = { + "scope": "exec:test", + "delegate_to": "a-service", + "delegate_scope": "read:all", + } + request_awaits = [client.get("/auth", params=params) for _ in range(100)] responses = await asyncio.gather(*request_awaits) assert responses[0].status_code == 200 token = Token.from_str(responses[0].headers["X-Auth-Request-Token"]) @@ -67,18 +56,12 @@ async def test_internal(client: AsyncClient, factory: Factory) -> None: assert r.status_code == 200 assert Token.from_str(r.headers["X-Auth-Request-Token"]) == token - request_awaits = [] - for _ in range(100): - request_awaits.append( - client.get( - "/auth", - params={ - "scope": "exec:test", - "delegate_to": "a-service", - "delegate_scope": "exec:test", - }, - ) - ) + params = { + "scope": "exec:test", + "delegate_to": "a-service", + "delegate_scope": "exec:test", + } + request_awaits = [client.get("/auth", params=params) for _ in range(100)] responses = await asyncio.gather(*request_awaits) assert responses[0].status_code == 200 new_token = Token.from_str(responses[0].headers["X-Auth-Request-Token"]) diff --git a/tests/support/ldap.py b/tests/support/ldap.py index 4ff302b32..30154a473 100644 --- a/tests/support/ldap.py +++ b/tests/support/ldap.py @@ -76,7 +76,8 @@ async def search( entries = self._entries[base][key] results = [] for entry in entries: - results.append({a: entry[a] for a in attrlist if a in entry}) + attributes = {a: entry[a] for a in attrlist if a in entry} + results.append(attributes) return results @asynccontextmanager