Coverage for object_streams/registry.py: 77%
92 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-02 17:07 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-02 17:07 +0000
1"""Model registration for object stream subscriptions."""
3from __future__ import annotations
5from collections.abc import Iterable
6from collections.abc import Iterator
7from collections.abc import Mapping
8from dataclasses import dataclass
9from dataclasses import field
10from typing import Any
12from django.apps import apps
13from django.db import models
15from object_streams.exceptions import AlreadyRegistered
16from object_streams.exceptions import NotRegistered
17from object_streams.filters import FilterSetLike
18from object_streams.filters import apply_filterset
19from object_streams.visibility import AllowAllVisibilityPolicy
20from object_streams.visibility import VisibilityPolicy
23__all__ = (
24 "ObjectStreamRegistration",
25 "ObjectStreamRegistry",
26 "register",
27 "registry",
28)
31def _normalize_model_label(model: type[models.Model] | str) -> str:
32 if isinstance(model, str):
33 return model
34 return model._meta.label
37def _resolve_model(model: type[models.Model] | str) -> type[models.Model]:
38 if not isinstance(model, str): 38 ↛ 41line 38 didn't jump to line 41 because the condition on line 38 was always true
39 return model
41 try:
42 app_label, model_name = model.split(".", 1)
43 except ValueError as exc:
44 msg = f"Model labels must use 'app_label.ModelName' format: {model!r}"
45 raise LookupError(msg) from exc
47 model_class = apps.get_model(app_label, model_name)
48 if model_class is None:
49 msg = f"No installed model matches {model!r}."
50 raise LookupError(msg)
51 return model_class
54@dataclass(frozen=True, slots=True)
55class ObjectStreamRegistration:
56 """Configuration used to subscribe to one Django model."""
58 model: type[models.Model]
59 filterset: type[FilterSetLike] | None = None
60 visibility: VisibilityPolicy = field(default_factory=AllowAllVisibilityPolicy)
61 sources: Iterable[Any] = field(default_factory=tuple)
62 facets: Iterable[str] = field(default_factory=lambda: ("object",))
64 def __post_init__(self):
65 sources = tuple(self.sources)
66 facets = {str(facet) for facet in self.facets}
67 for source in sources:
68 facet = getattr(source, "facet", None)
69 if facet is not None: 69 ↛ 67line 69 didn't jump to line 67 because the condition on line 69 was always true
70 facets.add(str(facet))
72 object.__setattr__(self, "sources", sources)
73 object.__setattr__(self, "facets", frozenset(facets or {"object"}))
75 @property
76 def model_label(self) -> str:
77 return self.model._meta.label
79 def get_queryset(
80 self,
81 user: Any,
82 filters: Mapping[str, Any] | None = None,
83 *,
84 action: str = "read",
85 request: Any = None,
86 ) -> models.QuerySet:
87 queryset = self.visibility.get_queryset(user, self.model, action=action)
88 return apply_filterset(self.filterset, queryset, filters, request=request)
91class ObjectStreamRegistry:
92 """In-memory registry for object stream model configuration."""
94 def __init__(self):
95 self._registrations: dict[str, ObjectStreamRegistration] = {}
97 def register(
98 self,
99 model: type[models.Model] | str | None = None,
100 *,
101 filterset: type[FilterSetLike] | None = None,
102 visibility: VisibilityPolicy | None = None,
103 sources: Iterable[Any] = (),
104 facets: Iterable[str] | None = None,
105 ):
106 def do_register(model_class: type[models.Model]) -> ObjectStreamRegistration:
107 registration = ObjectStreamRegistration(
108 model=model_class,
109 filterset=filterset,
110 visibility=visibility or AllowAllVisibilityPolicy(),
111 sources=tuple(sources),
112 facets=tuple(facets or ("object",)),
113 )
114 label = registration.model_label
115 if label in self._registrations:
116 msg = f"{label} is already registered for object streams."
117 raise AlreadyRegistered(msg)
118 self._registrations[label] = registration
119 return registration
121 if model is None: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 return do_register
123 return do_register(_resolve_model(model))
125 def unregister(self, model: type[models.Model] | str) -> None:
126 label = _normalize_model_label(model)
127 try:
128 del self._registrations[label]
129 except KeyError as exc:
130 msg = f"{label} is not registered for object streams."
131 raise NotRegistered(msg) from exc
133 def get(self, model: type[models.Model] | str) -> ObjectStreamRegistration:
134 label = _normalize_model_label(model)
135 try:
136 return self._registrations[label]
137 except KeyError as exc:
138 msg = f"{label} is not registered for object streams."
139 raise NotRegistered(msg) from exc
141 def clear(self) -> None:
142 self._registrations.clear()
144 def __contains__(self, model: type[models.Model] | str) -> bool:
145 return _normalize_model_label(model) in self._registrations
147 def __iter__(self) -> Iterator[ObjectStreamRegistration]:
148 return iter(self._registrations.values())
151registry = ObjectStreamRegistry()
152register = registry.register