Coverage for src/lexigram/admin/relations/manager.py: 0%
38 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
1"""Relation manager for lexigram-admin.
3Provides base class for managing entity relationships in admin views.
4"""
6from __future__ import annotations
8from abc import ABC, abstractmethod
9from typing import TYPE_CHECKING, Any, ClassVar
11if TYPE_CHECKING:
12 from lexigram.ui.columns.types import Column
13from lexigram.admin.exceptions import PermissionDeniedError
14from lexigram.di.decorators import inject
15from lexigram.result import Ok, Result
18@inject
19class AbstractRelationManager(ABC):
20 """Base class for managing entity relationships.
22 Subclasses define how to display and query related entities
23 for a parent resource.
25 Example:
26 class UserPetsRelationManager(AbstractRelationManager):
27 relationship_name = "pets"
29 @classmethod
30 def table(cls, table_config=None):
31 return [
32 TextColumn("name").sortable(),
33 BadgeColumn("species"),
34 ]
36 async def get_query(self):
37 return await pet_service.list(user_id=self.parent_id)
38 """
40 relationship_name: ClassVar[str] = ""
42 def __init__(
43 self,
44 parent_id: Any = None,
45 parent: Any = None,
46 data_source: Any = None,
47 ):
48 """Initialize relation manager.
50 Args:
51 parent_id: ID of the parent entity
52 parent: The parent entity object (optional)
53 data_source: Optional data source used for persisting
54 pivot/relation operations. Subclasses may also attach
55 one later via :meth:`set_data_source`.
56 """
57 self.parent_id = parent_id
58 self.parent = parent
59 self._data_source = data_source
61 def set_data_source(self, data_source: Any) -> None:
62 """Attach a data source for pivot persistence operations."""
63 self._data_source = data_source
65 async def get_parent(self) -> Any:
66 """Resolve the parent entity for this relationship.
68 Returns:
69 The resolved parent entity, or ``None`` when no parent object
70 is declared.
72 Note:
73 Override to resolve ``self.parent_id`` through a data source.
74 """
75 return self.parent
77 def can_view_parent(
78 self, parent: Any, user: Any | None = None
79 ) -> Result[None, PermissionDeniedError]:
80 """Check whether the user may view the given parent entity.
82 Defaults to allow; override to declare per-parent read policy.
84 Args:
85 parent: The resolved parent entity.
86 user: The requesting admin user.
88 Returns:
89 ``Ok(None)`` when viewing is permitted, ``Err`` with a
90 ``PermissionDeniedError`` when denied.
91 """
92 return Ok(None)
94 @classmethod
95 @abstractmethod
96 def table(cls, table_config: Any = None) -> list[Column]:
97 """Define columns for displaying related entities.
99 Args:
100 table_config: Optional table configuration
102 Returns:
103 List of Column instances
104 """
105 ...
107 @abstractmethod
108 async def get_query(self) -> list[Any]:
109 """Get related entities for the parent.
111 Returns:
112 List of related entities
113 """
114 ...
116 async def count(self) -> int:
117 """Count related entities.
119 Returns:
120 Number of related entities
121 """
122 items = await self.get_query()
123 return len(items) if items else 0
125 async def get_items(
126 self,
127 *,
128 page: int = 1,
129 per_page: int = 20,
130 **filters: Any,
131 ) -> list[Any]:
132 """Get paginated related entities.
134 Args:
135 page: Page number (1-indexed)
136 per_page: Items per page
137 **filters: Additional filters
139 Returns:
140 List of related entities for the page
141 """
142 items = await self.get_query()
143 if not items:
144 return []
146 start = (page - 1) * per_page
147 end = start + per_page
148 return items[start:end]
150 @classmethod
151 def get_relationship_name(cls) -> str:
152 """Get the name of this relationship."""
153 return cls.relationship_name or cls.__name__.lower().replace(
154 "relationmanager",
155 "",
156 )
159__all__ = ["AbstractRelationManager"]