django-admin-grpc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_admin_grpc/__init__.py +71 -0
- django_admin_grpc/adapters.py +153 -0
- django_admin_grpc/admin.py +938 -0
- django_admin_grpc/apps.py +8 -0
- django_admin_grpc/exceptions.py +95 -0
- django_admin_grpc/filters.py +269 -0
- django_admin_grpc/forms.py +303 -0
- django_admin_grpc/interceptors.py +96 -0
- django_admin_grpc/mappers.py +149 -0
- django_admin_grpc/models.py +287 -0
- django_admin_grpc/paginator.py +49 -0
- django_admin_grpc/registry.py +51 -0
- django_admin_grpc/resources.py +267 -0
- django_admin_grpc/settings.py +41 -0
- django_admin_grpc/templates/django_admin_grpc/change_form.html +130 -0
- django_admin_grpc/templates/django_admin_grpc/cursor_pagination.html +14 -0
- django_admin_grpc/templates/django_admin_grpc/delete_confirm.html +41 -0
- django_admin_grpc/widgets.py +34 -0
- django_admin_grpc-0.1.0.dist-info/METADATA +578 -0
- django_admin_grpc-0.1.0.dist-info/RECORD +22 -0
- django_admin_grpc-0.1.0.dist-info/WHEEL +4 -0
- django_admin_grpc-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
django-grpc-admin
|
|
3
|
+
|
|
4
|
+
A reusable Django package for creating admin interfaces backed by gRPC services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django_admin_grpc.adapters import BaseGrpcServiceAdapter
|
|
8
|
+
from django_admin_grpc.exceptions import (
|
|
9
|
+
GrpcAdminError,
|
|
10
|
+
GrpcDeadlineExceededError,
|
|
11
|
+
GrpcInvalidArgumentError,
|
|
12
|
+
GrpcNotFoundError,
|
|
13
|
+
GrpcPermissionDeniedError,
|
|
14
|
+
GrpcUnavailableError,
|
|
15
|
+
)
|
|
16
|
+
from django_admin_grpc.filters import (
|
|
17
|
+
GrpcBooleanFieldListFilter,
|
|
18
|
+
GrpcChoicesFieldListFilter,
|
|
19
|
+
GrpcSimpleListFilter,
|
|
20
|
+
GrpcTextInputFilter,
|
|
21
|
+
create_grpc_filter_spec,
|
|
22
|
+
)
|
|
23
|
+
from django_admin_grpc.mappers import BaseGrpcMapper
|
|
24
|
+
from django_admin_grpc.paginator import GrpcPaginator, PagedResult
|
|
25
|
+
from django_admin_grpc.registry import AdapterRegistry, adapter_registry
|
|
26
|
+
from django_admin_grpc.resources import (
|
|
27
|
+
BaseFieldConfig,
|
|
28
|
+
BaseGrpcResource,
|
|
29
|
+
BooleanFieldConfig,
|
|
30
|
+
CharFieldConfig,
|
|
31
|
+
ChoicesFieldConfig,
|
|
32
|
+
DateFieldConfig,
|
|
33
|
+
DateTimeFieldConfig,
|
|
34
|
+
FKFieldConfig,
|
|
35
|
+
FloatFieldConfig,
|
|
36
|
+
IntegerFieldConfig,
|
|
37
|
+
TextFieldConfig,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"BaseFieldConfig",
|
|
44
|
+
"BaseGrpcResource",
|
|
45
|
+
"BaseGrpcServiceAdapter",
|
|
46
|
+
"BaseGrpcMapper",
|
|
47
|
+
"BooleanFieldConfig",
|
|
48
|
+
"CharFieldConfig",
|
|
49
|
+
"ChoicesFieldConfig",
|
|
50
|
+
"DateFieldConfig",
|
|
51
|
+
"DateTimeFieldConfig",
|
|
52
|
+
"FKFieldConfig",
|
|
53
|
+
"FloatFieldConfig",
|
|
54
|
+
"IntegerFieldConfig",
|
|
55
|
+
"TextFieldConfig",
|
|
56
|
+
"GrpcAdminError",
|
|
57
|
+
"GrpcNotFoundError",
|
|
58
|
+
"GrpcPermissionDeniedError",
|
|
59
|
+
"GrpcInvalidArgumentError",
|
|
60
|
+
"GrpcUnavailableError",
|
|
61
|
+
"GrpcDeadlineExceededError",
|
|
62
|
+
"GrpcPaginator",
|
|
63
|
+
"PagedResult",
|
|
64
|
+
"GrpcBooleanFieldListFilter",
|
|
65
|
+
"GrpcChoicesFieldListFilter",
|
|
66
|
+
"GrpcSimpleListFilter",
|
|
67
|
+
"GrpcTextInputFilter",
|
|
68
|
+
"create_grpc_filter_spec",
|
|
69
|
+
"AdapterRegistry",
|
|
70
|
+
"adapter_registry",
|
|
71
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base gRPC service adapter interface.
|
|
3
|
+
|
|
4
|
+
Concrete adapters subclass ``BaseGrpcServiceAdapter`` and implement ``list()``,
|
|
5
|
+
``get()`` and optionally ``create()``, ``update()``, ``delete()``.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
13
|
+
|
|
14
|
+
import grpc
|
|
15
|
+
|
|
16
|
+
from django_admin_grpc.exceptions import map_grpc_error
|
|
17
|
+
from django_admin_grpc.paginator import PagedResult
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from django_admin_grpc.resources import BaseGrpcResource
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseGrpcServiceAdapter(ABC):
|
|
26
|
+
"""
|
|
27
|
+
Abstract interface between Django admin and a remote gRPC service.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
service_name: Human-readable name used by the registry.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
service_name: str = ""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def list(
|
|
37
|
+
self,
|
|
38
|
+
resource_class: type[BaseGrpcResource],
|
|
39
|
+
page: int = 1,
|
|
40
|
+
page_size: int = 25,
|
|
41
|
+
filters: dict[str, Any] | None = None,
|
|
42
|
+
) -> PagedResult:
|
|
43
|
+
"""
|
|
44
|
+
Fetch a page of entities.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
resource_class: The resource class to instantiate for each row.
|
|
48
|
+
page: 1-indexed page number.
|
|
49
|
+
page_size: Items per page.
|
|
50
|
+
filters: Optional query/filter dictionary.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A ``PagedResult`` containing items and pagination metadata.
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def get(
|
|
59
|
+
self,
|
|
60
|
+
resource_class: type[BaseGrpcResource],
|
|
61
|
+
pk: str,
|
|
62
|
+
) -> BaseGrpcResource | None:
|
|
63
|
+
"""
|
|
64
|
+
Fetch a single entity by primary key.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
resource_class: The resource class to instantiate.
|
|
68
|
+
pk: Primary key value.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A resource instance, or ``None`` if not found.
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def create(
|
|
76
|
+
self,
|
|
77
|
+
resource_class: type[BaseGrpcResource],
|
|
78
|
+
data: dict[str, Any],
|
|
79
|
+
) -> BaseGrpcResource:
|
|
80
|
+
"""Create a new entity via gRPC."""
|
|
81
|
+
raise NotImplementedError(
|
|
82
|
+
f"{self.__class__.__name__} does not support create"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def update(
|
|
86
|
+
self,
|
|
87
|
+
resource_class: type[BaseGrpcResource],
|
|
88
|
+
pk: str,
|
|
89
|
+
data: dict[str, Any],
|
|
90
|
+
) -> BaseGrpcResource:
|
|
91
|
+
"""Update an existing entity via gRPC."""
|
|
92
|
+
raise NotImplementedError(
|
|
93
|
+
f"{self.__class__.__name__} does not support update"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def delete(
|
|
97
|
+
self,
|
|
98
|
+
resource_class: type[BaseGrpcResource],
|
|
99
|
+
pk: str,
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""Delete an entity via gRPC."""
|
|
102
|
+
raise NotImplementedError(
|
|
103
|
+
f"{self.__class__.__name__} does not support delete"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def supports_create(self) -> bool:
|
|
108
|
+
return type(self).create is not BaseGrpcServiceAdapter.create
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def supports_update(self) -> bool:
|
|
112
|
+
return type(self).update is not BaseGrpcServiceAdapter.update
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def supports_delete(self) -> bool:
|
|
116
|
+
return type(self).delete is not BaseGrpcServiceAdapter.delete
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
"""Release any held connections. Override if needed."""
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def _wrap_channel(self, channel: grpc.Channel) -> grpc.Channel:
|
|
123
|
+
"""
|
|
124
|
+
Wrap a raw gRPC channel with the trace interceptor.
|
|
125
|
+
|
|
126
|
+
Must be called **inside** the ``if self._channel is None:`` guard in
|
|
127
|
+
concrete adapters so the channel is not double-wrapped.
|
|
128
|
+
"""
|
|
129
|
+
from django_admin_grpc.interceptors import TraceClientInterceptor
|
|
130
|
+
|
|
131
|
+
provider = self._trace_context_provider()
|
|
132
|
+
return grpc.intercept_channel(
|
|
133
|
+
channel, TraceClientInterceptor(trace_context_provider=provider)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _trace_context_provider(self) -> Callable[[], dict[str, str]]:
|
|
137
|
+
"""Return the configured trace-context callable, or a no-op."""
|
|
138
|
+
from django_admin_grpc.settings import get_setting
|
|
139
|
+
|
|
140
|
+
provider = get_setting("GRPC_ADMIN_TRACE_CONTEXT_PROVIDER")
|
|
141
|
+
if provider is None:
|
|
142
|
+
return lambda: {}
|
|
143
|
+
if callable(provider):
|
|
144
|
+
return cast(Callable[[], dict[str, str]], provider)
|
|
145
|
+
# Django-style dotted path
|
|
146
|
+
from django.utils.module_loading import import_string
|
|
147
|
+
|
|
148
|
+
return cast(Callable[[], dict[str, str]], import_string(provider))
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _map_rpc_error(exc: grpc.RpcError) -> Exception:
|
|
152
|
+
"""Map a gRPC error to a typed exception. Callers should ``raise`` the result."""
|
|
153
|
+
return map_grpc_error(exc)
|