Compare commits

...

1 Commits

Author SHA1 Message Date
Trenton H
efda9cf892 Adds stronger typing to the status managers 2026-03-10 14:21:45 -07:00
5 changed files with 103 additions and 33 deletions

View File

@@ -138,14 +138,12 @@ class ConsumerPluginMixin:
message,
current_progress,
max_progress,
extra_args={
"document_id": document_id,
"owner_id": self.metadata.owner_id if self.metadata.owner_id else None,
"users_can_view": (self.metadata.view_users or [])
+ (self.metadata.change_users or []),
"groups_can_view": (self.metadata.view_groups or [])
+ (self.metadata.change_groups or []),
},
document_id=document_id,
owner_id=self.metadata.owner_id if self.metadata.owner_id else None,
users_can_view=(self.metadata.view_users or [])
+ (self.metadata.change_users or []),
groups_can_view=(self.metadata.view_groups or [])
+ (self.metadata.change_groups or []),
)
def _fail(

View File

@@ -1,6 +1,9 @@
import enum
from collections.abc import Mapping
from typing import TYPE_CHECKING
from typing import Literal
from typing import Self
from typing import TypeAlias
from typing import TypedDict
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
@@ -16,6 +19,59 @@ class ProgressStatusOptions(enum.StrEnum):
FAILED = "FAILED"
class PermissionsData(TypedDict, total=False):
"""Permission fields included in status messages for access control."""
owner_id: int | None
users_can_view: list[int]
groups_can_view: list[int]
class ProgressUpdateData(TypedDict):
filename: str | None
task_id: str | None
current_progress: int
max_progress: int
status: str
message: str
document_id: int | None
owner_id: int | None
users_can_view: list[int]
groups_can_view: list[int]
class StatusUpdatePayload(TypedDict):
type: Literal["status_update"]
data: ProgressUpdateData
class DocumentsDeletedData(TypedDict):
documents: list[int]
class DocumentsDeletedPayload(TypedDict):
type: Literal["documents_deleted"]
data: DocumentsDeletedData
class DocumentUpdatedData(TypedDict):
document_id: int
modified: str
owner_id: int | None
users_can_view: list[int]
groups_can_view: list[int]
class DocumentUpdatedPayload(TypedDict):
type: Literal["document_updated"]
data: DocumentUpdatedData
WebsocketPayload: TypeAlias = (
StatusUpdatePayload | DocumentsDeletedPayload | DocumentUpdatedPayload
)
class BaseStatusManager:
"""
Handles sending of progress information via the channel layer, with proper management
@@ -25,11 +81,11 @@ class BaseStatusManager:
def __init__(self) -> None:
self._channel: RedisPubSubChannelLayer | None = None
def __enter__(self):
def __enter__(self) -> Self:
self.open()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
self.close()
def open(self) -> None:
@@ -48,7 +104,7 @@ class BaseStatusManager:
async_to_sync(self._channel.flush)
self._channel = None
def send(self, payload: Mapping[str, object]) -> None:
def send(self, payload: WebsocketPayload) -> None:
# Ensure the layer is open
self.open()
@@ -72,36 +128,36 @@ class ProgressManager(BaseStatusManager):
message: str,
current_progress: int,
max_progress: int,
extra_args: dict[str, str | int | None] | None = None,
*,
document_id: int | None = None,
owner_id: int | None = None,
users_can_view: list[int] | None = None,
groups_can_view: list[int] | None = None,
) -> None:
data: dict[str, object] = {
data: ProgressUpdateData = {
"filename": self.filename,
"task_id": self.task_id,
"current_progress": current_progress,
"max_progress": max_progress,
"status": status,
"message": message,
"document_id": document_id,
"owner_id": owner_id,
"users_can_view": users_can_view or [],
"groups_can_view": groups_can_view or [],
}
if extra_args is not None:
data.update(extra_args)
payload: dict[str, object] = {
"type": "status_update",
"data": data,
}
payload: StatusUpdatePayload = {"type": "status_update", "data": data}
self.send(payload)
class DocumentsStatusManager(BaseStatusManager):
def send_documents_deleted(self, documents: list[int]) -> None:
payload: dict[str, object] = {
payload: DocumentsDeletedPayload = {
"type": "documents_deleted",
"data": {
"documents": documents,
},
}
self.send(payload)
def send_document_updated(
@@ -113,7 +169,7 @@ class DocumentsStatusManager(BaseStatusManager):
users_can_view: list[int] | None = None,
groups_can_view: list[int] | None = None,
) -> None:
payload: dict[str, object] = {
payload: DocumentUpdatedPayload = {
"type": "document_updated",
"data": {
"document_id": document_id,
@@ -123,5 +179,4 @@ class DocumentsStatusManager(BaseStatusManager):
"groups_can_view": groups_can_view or [],
},
}
self.send(payload)

View File

@@ -429,7 +429,11 @@ class DummyProgressManager:
message: str,
current_progress: int,
max_progress: int,
extra_args: dict[str, str | int] | None = None,
*,
document_id: int | None = None,
owner_id: int | None = None,
users_can_view: list[int] | None = None,
groups_can_view: list[int] | None = None,
) -> None:
# Ensure the layer is open
self.open()
@@ -443,9 +447,10 @@ class DummyProgressManager:
"max_progress": max_progress,
"status": status,
"message": message,
"document_id": document_id,
"owner_id": owner_id,
"users_can_view": users_can_view or [],
"groups_can_view": groups_can_view or [],
},
}
if extra_args is not None:
payload["data"].update(extra_args)
self.payloads.append(payload)

View File

@@ -1,15 +1,21 @@
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from typing import Any
from channels.generic.websocket import AsyncWebsocketConsumer
if TYPE_CHECKING:
from documents.plugins.helpers import PermissionsData
class StatusConsumer(AsyncWebsocketConsumer):
def _authenticated(self) -> bool:
user: Any = self.scope.get("user")
return user is not None and user.is_authenticated
async def _can_view(self, data: dict[str, Any]) -> bool:
async def _can_view(self, data: PermissionsData) -> bool:
user: Any = self.scope.get("user")
if user is None:
return False

View File

@@ -199,7 +199,10 @@ class TestWebSockets:
"Test message",
1,
10,
extra_args={"foo": "bar"},
document_id=42,
owner_id=1,
users_can_view=[2, 3],
groups_can_view=[4],
)
assert mock_group_send.call_args[0][1] == {
@@ -211,7 +214,10 @@ class TestWebSockets:
"max_progress": 10,
"status": ProgressStatusOptions.STARTED,
"message": "Test message",
"foo": "bar",
"document_id": 42,
"owner_id": 1,
"users_can_view": [2, 3],
"groups_can_view": [4],
},
}