django-bananas 2.2__py3-none-any.whl → 2.4__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.
bananas/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- VERSION = (2, 2, 0, "final", 0)
1
+ VERSION = (2, 4, 0, "final", 0)
2
2
 
3
3
 
4
4
  def get_version() -> str:
@@ -63,9 +63,9 @@ class BananasAPI:
63
63
  if admin is not None:
64
64
  meta.update(
65
65
  {
66
- key: getattr(admin, key) # type: ignore[misc, call-overload]
66
+ key: getattr(admin, key)
67
67
  for key in filter(
68
- lambda key: key in meta, # type: ignore[arg-type]
68
+ lambda key: key in meta,
69
69
  admin.__dict__.keys(),
70
70
  )
71
71
  }
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Type, cast
3
3
  from rest_framework.viewsets import ViewSetMixin
4
4
 
5
5
  if TYPE_CHECKING:
6
- from ..mixins import BananasAPI
6
+ from bananas.admin.api.mixins import BananasAPI
7
7
 
8
8
 
9
9
  class BananasBaseRouter:
@@ -13,7 +13,8 @@ from rest_framework.request import Request
13
13
  from rest_framework.routers import SimpleRouter
14
14
  from rest_framework.schemas.coreapi import is_custom_action
15
15
 
16
- from ..versioning import BananasVersioning
16
+ from bananas.admin.api.versioning import BananasVersioning
17
+
17
18
  from .base import BananasBaseRouter
18
19
 
19
20
 
@@ -1,7 +1,7 @@
1
1
  from django.urls import include, re_path
2
2
 
3
- from .. import views
4
- from ..router import register, router
3
+ from bananas.admin.api import views
4
+ from bananas.admin.api.router import register, router
5
5
 
6
6
  register(views.LoginAPI)
7
7
  register(views.LogoutAPI)
@@ -1,5 +1,5 @@
1
1
  from types import ModuleType
2
- from typing import Dict, Sequence
2
+ from typing import ClassVar, Dict, Sequence
3
3
 
4
4
  from rest_framework.request import Request
5
5
  from rest_framework.versioning import NamespaceVersioning
@@ -10,12 +10,11 @@ __versions__ = [v1_0]
10
10
 
11
11
 
12
12
  class BananasVersioning(NamespaceVersioning):
13
-
14
13
  default_version: str = v1_0.__version__
15
14
  allowed_versions: Sequence[str] = tuple(
16
15
  version.__version__ for version in __versions__
17
16
  )
18
- version_map: Dict[str, ModuleType] = {
17
+ version_map: ClassVar[Dict[str, ModuleType]] = {
19
18
  version.__version__: version for version in __versions__
20
19
  }
21
20
 
@@ -1,3 +1,5 @@
1
+ from typing import ClassVar, List
2
+
1
3
  from django.contrib.auth import (
2
4
  login as auth_login,
3
5
  logout as auth_logout,
@@ -11,7 +13,8 @@ from rest_framework.permissions import AllowAny
11
13
  from rest_framework.request import Request
12
14
  from rest_framework.response import Response
13
15
 
14
- from .i18n import RawTranslationCatalog
16
+ from bananas.admin.i18n import RawTranslationCatalog
17
+
15
18
  from .mixins import BananasAPI
16
19
  from .permissions import IsAnonymous
17
20
  from .schemas import schema
@@ -29,7 +32,6 @@ class BananasAdminAPI(BananasAPI, viewsets.GenericViewSet):
29
32
 
30
33
 
31
34
  class LoginAPI(BananasAdminAPI):
32
-
33
35
  name = _("Log in") # type: ignore[assignment]
34
36
  basename = "login"
35
37
  permission_classes = (IsAnonymous,)
@@ -60,7 +62,6 @@ class LoginAPI(BananasAdminAPI):
60
62
 
61
63
 
62
64
  class LogoutAPI(BananasAPI, viewsets.ViewSet):
63
-
64
65
  name = _("Log out") # type: ignore[assignment]
65
66
  basename = "logout"
66
67
 
@@ -77,11 +78,10 @@ class LogoutAPI(BananasAPI, viewsets.ViewSet):
77
78
 
78
79
 
79
80
  class MeAPI(BananasAdminAPI):
80
-
81
81
  serializer_class = UserSerializer
82
82
 
83
83
  class Admin:
84
- exclude_tags = ["navigation"]
84
+ exclude_tags: ClassVar[List[str]] = ["navigation"]
85
85
 
86
86
  @schema(responses={200: UserSerializer})
87
87
  def list(self, request: Request) -> Response:
@@ -93,7 +93,6 @@ class MeAPI(BananasAdminAPI):
93
93
 
94
94
 
95
95
  class ChangePasswordAPI(BananasAdminAPI):
96
-
97
96
  name = _("Change password") # type: ignore[assignment]
98
97
  basename = "change_password"
99
98
  serializer_class = PasswordChangeSerializer # Placeholder for schema
@@ -121,13 +120,12 @@ class ChangePasswordAPI(BananasAdminAPI):
121
120
 
122
121
 
123
122
  class TranslationAPI(BananasAdminAPI):
124
-
125
123
  name = _("Translation catalog") # type: ignore[assignment]
126
124
  basename = "i18n"
127
125
  permission_classes = (AllowAny,)
128
126
 
129
127
  class Admin:
130
- exclude_tags = ["navigation"]
128
+ exclude_tags: ClassVar[List[str]] = ["navigation"]
131
129
 
132
130
  @schema(responses={200: ""})
133
131
  def list(self, request: Request) -> Response:
@@ -2,6 +2,7 @@ import re
2
2
  from typing import (
3
3
  Any,
4
4
  Callable,
5
+ ClassVar,
5
6
  Dict,
6
7
  List,
7
8
  Optional,
@@ -31,7 +32,7 @@ from django.utils.safestring import SafeText, mark_safe
31
32
  from django.utils.translation import gettext_lazy as _
32
33
  from django.views.generic import View
33
34
 
34
- from ..environment import env
35
+ from bananas.environment import env
35
36
 
36
37
  __all__ = ["ModelAdminView", "ViewTool", "AdminView", "register", "site"]
37
38
 
@@ -41,7 +42,7 @@ MT = TypeVar("MT", bound=Model)
41
42
 
42
43
  class ExtendedAdminSite(AdminSite):
43
44
  enable_nav_sidebar = False
44
- default_settings = {
45
+ default_settings: ClassVar[Dict[str, Any]] = {
45
46
  "INHERIT_REGISTERED_MODELS": env.get_bool(
46
47
  "DJANGO_ADMIN_INHERIT_REGISTERED_MODELS", True
47
48
  ),
@@ -84,10 +85,8 @@ class ModelAdminView(ModelAdmin):
84
85
  @cached_property
85
86
  def access_permission(self) -> str:
86
87
  meta = self.model._meta
87
- return "{app_label}.{codename}".format(
88
- app_label=meta.app_label,
89
- codename=meta.permissions[0][0], # First perm codename
90
- )
88
+ codename = meta.permissions[0][0] # First perm codename
89
+ return f"{meta.app_label}.{codename}"
91
90
 
92
91
  def get_urls(self) -> List[URLPattern]:
93
92
  app_label = self.model._meta.app_label
@@ -122,7 +121,8 @@ class ModelAdminView(ModelAdmin):
122
121
 
123
122
  admin_login_url = reverse_lazy("admin:login")
124
123
  view = user_passes_test(
125
- lambda u: u.is_active and hasattr(u, "is_staff") and u.is_staff, login_url=admin_login_url # type: ignore[arg-type, return-value]
124
+ lambda u: u.is_active and hasattr(u, "is_staff") and u.is_staff,
125
+ login_url=admin_login_url,
126
126
  )(view)
127
127
  view = permission_required(perm, login_url=admin_login_url)(view)
128
128
  return view
@@ -133,12 +133,12 @@ class ModelAdminView(ModelAdmin):
133
133
  return perm
134
134
 
135
135
  def has_module_permission(self, request: HttpRequest) -> bool:
136
- return request.user.has_perm(self.access_permission)
136
+ return bool(request.user.has_perm(self.access_permission))
137
137
 
138
138
  def has_change_permission(
139
139
  self, request: HttpRequest, obj: Optional[MT] = None
140
140
  ) -> bool:
141
- return request.user.has_perm(self.access_permission)
141
+ return bool(request.user.has_perm(self.access_permission))
142
142
 
143
143
  # TODO: Remove obj?
144
144
  def has_add_permission(
@@ -175,8 +175,7 @@ def register(
175
175
  *,
176
176
  admin_site: Optional[AdminSite] = None,
177
177
  admin_class: Type[ModelAdmin] = ModelAdminView,
178
- ) -> Type["AdminView"]:
179
- ...
178
+ ) -> Type["AdminView"]: ...
180
179
 
181
180
 
182
181
  # Call with parenthesis: @register()
@@ -186,8 +185,7 @@ def register(
186
185
  *,
187
186
  admin_site: Optional[AdminSite] = None,
188
187
  admin_class: Type[ModelAdmin] = ModelAdminView,
189
- ) -> Callable[[Type["AdminView"]], Type["AdminView"]]:
190
- ...
188
+ ) -> Callable[[Type["AdminView"]], Type["AdminView"]]: ...
191
189
 
192
190
 
193
191
  def register(
@@ -236,14 +234,14 @@ def register(
236
234
  inner_view.verbose_name = verbose_name
237
235
 
238
236
  access_perm_codename = "can_access_" + model_name.lower()
239
- access_perm_name = _("Can access {verbose_name}").format(
237
+ access_perm_name = str(_("Can access {verbose_name}")).format(
240
238
  verbose_name=verbose_name
241
239
  )
242
240
  # The first permission here is expected to be
243
241
  # the general access permission.
244
- permissions = tuple(
245
- [(access_perm_codename, access_perm_name)]
246
- + list(getattr(inner_view, "permissions", []))
242
+ permissions = (
243
+ (access_perm_codename, access_perm_name),
244
+ *list(getattr(inner_view, "permissions", [])),
247
245
  )
248
246
 
249
247
  model = type(
@@ -303,9 +301,11 @@ class ViewTool:
303
301
 
304
302
 
305
303
  class AdminView(View):
306
- tools: Optional[List[Union[Tuple[str, str], Tuple[str, str, str], ViewTool]]] = None
307
- action: Optional[str] = None
308
- admin: Optional[ModelAdminView] = None
304
+ tools: ClassVar[
305
+ Optional[List[Union[Tuple[str, str], Tuple[str, str, str], ViewTool]]]
306
+ ] = None
307
+ action: ClassVar[Optional[str]] = None
308
+ admin: ClassVar[Optional[ModelAdminView]] = None
309
309
 
310
310
  label: str
311
311
  verbose_name: str
@@ -354,8 +354,8 @@ class AdminView(View):
354
354
  # Mypy doesn't change type on a len(...) call
355
355
  # See: https://github.com/python/mypy/issues/1178
356
356
  if len(tool) == 3:
357
- tool, perm = cast(Tuple[str, str, str], tool)[:-1], tool[-1]
358
- text, link = cast(Tuple[str, str], tool)
357
+ tool, perm = tool[:-1], tool[-1]
358
+ text, link = tool
359
359
  tool = ViewTool(text, link, perm=perm)
360
360
  else:
361
361
  # Assume ViewTool
@@ -385,7 +385,7 @@ class AdminView(View):
385
385
 
386
386
  def has_permission(self, perm: str) -> bool:
387
387
  perm = self.get_permission(perm)
388
- return self.request.user.has_perm(perm)
388
+ return bool(self.request.user.has_perm(perm))
389
389
 
390
390
  def has_access(self) -> bool:
391
391
  assert self.admin is not None
@@ -4,7 +4,6 @@ from django.views.i18n import JavaScriptCatalog
4
4
 
5
5
 
6
6
  class RawTranslationCatalog(JavaScriptCatalog):
7
-
8
7
  domain = "django"
9
8
 
10
9
  def render_to_response(self, context: Dict[str, Any], **response_kwargs: Any) -> Dict[str, Any]: # type: ignore[override]
bananas/drf/fencing.py CHANGED
@@ -5,6 +5,7 @@ from functools import wraps
5
5
  from typing import (
6
6
  Any,
7
7
  Callable,
8
+ Final,
8
9
  FrozenSet,
9
10
  Generic,
10
11
  List,
@@ -24,7 +25,7 @@ from rest_framework.request import Request
24
25
  from rest_framework.response import Response
25
26
  from rest_framework.serializers import BaseSerializer, ModelSerializer
26
27
  from rest_framework.viewsets import GenericViewSet
27
- from typing_extensions import Final, Protocol, final
28
+ from typing_extensions import Protocol, final
28
29
 
29
30
  from bananas.admin.api.schemas.yasg import BananasSwaggerSchema
30
31
  from bananas.models import TimeStampedModel
@@ -55,14 +56,18 @@ class Fence(abc.ABC, Generic[InstanceType, TokenType]):
55
56
  compare: Callable[[TokenType, TokenType], bool],
56
57
  get_version: Callable[[InstanceType], Optional[TokenType]],
57
58
  openapi_parameter: openapi.Parameter,
58
- rejection: Exception = errors.PreconditionFailed(
59
- "The resource does not fulfill the given preconditions"
60
- ),
59
+ rejection: Optional[Exception] = None,
61
60
  ) -> None:
62
61
  self._get_token: Final = get_token
63
62
  self._compare: Final = compare
64
63
  self._get_version: Final = get_version
65
- self._rejection: Final = rejection
64
+ self._rejection: Final = (
65
+ rejection
66
+ if rejection is not None
67
+ else errors.PreconditionFailed(
68
+ "The resource does not fulfill the given preconditions"
69
+ )
70
+ )
66
71
  self.openapi_parameter: Final = openapi_parameter
67
72
 
68
73
  def check(self, request: Request, instance: InstanceType) -> bool:
@@ -93,7 +98,7 @@ class FenceAwareSwaggerAutoSchema(BananasSwaggerSchema):
93
98
  isinstance(self.view, FencedUpdateModelMixin)
94
99
  and self.method in self.update_methods
95
100
  ):
96
- return parameters + [self.view.fence.openapi_parameter]
101
+ return [*parameters, self.view.fence.openapi_parameter]
97
102
  return parameters
98
103
 
99
104
 
@@ -101,15 +106,13 @@ _MT_co = TypeVar("_MT_co", bound=Model, covariant=True)
101
106
 
102
107
 
103
108
  class UsesQuerySet(Protocol[_MT_co]):
104
- def get_queryset(self) -> "QuerySet[_MT_co]":
105
- ...
109
+ def get_queryset(self) -> "QuerySet[_MT_co]": ...
106
110
 
107
111
 
108
112
  class FencedUpdateModelMixin(UpdateModelMixin, abc.ABC):
109
113
  @property
110
114
  @abc.abstractmethod
111
- def fence(self) -> Fence:
112
- ...
115
+ def fence(self) -> Fence: ...
113
116
 
114
117
  # django-restframework uses an "advanced self-type" on self in
115
118
  # perform_update() which subtly breaks subclassing. We try to remedy this by
@@ -141,7 +144,7 @@ def header_date_parser(header: str) -> Callable[[Request], datetime.datetime]:
141
144
  try:
142
145
  return parse_header_datetime(request, header)
143
146
  except HeaderError as e:
144
- raise e.as_api_error()
147
+ raise e.as_api_error() from e
145
148
 
146
149
  return parse
147
150
 
@@ -183,7 +186,7 @@ def header_etag_parser(header: str) -> Callable[[Request], FrozenSet[str]]:
183
186
  try:
184
187
  return parse_header_etags(request, header)
185
188
  except HeaderError as e:
186
- raise e.as_api_error()
189
+ raise e.as_api_error() from e
187
190
 
188
191
  return parse
189
192
 
@@ -192,7 +195,7 @@ T = TypeVar("T")
192
195
 
193
196
 
194
197
  def as_set(
195
- fn: Callable[[InstanceType], Optional[T]]
198
+ fn: Callable[[InstanceType], Optional[T]],
196
199
  ) -> Callable[[InstanceType], Optional[FrozenSet[T]]]:
197
200
  @wraps(fn)
198
201
  def wrapper(instance: InstanceType) -> Optional[FrozenSet[T]]:
@@ -203,7 +206,7 @@ def as_set(
203
206
 
204
207
 
205
208
  def allow_if_match(
206
- version_getter: Callable[[InstanceType], Optional[str]]
209
+ version_getter: Callable[[InstanceType], Optional[str]],
207
210
  ) -> Fence[InstanceType, FrozenSet[str]]:
208
211
  return Fence(
209
212
  get_token=header_etag_parser("If-Match"),
bananas/drf/utils.py CHANGED
@@ -39,8 +39,8 @@ class InvalidHeader(HeaderError):
39
39
  def parse_header_datetime(request: Request, header: str) -> datetime.datetime:
40
40
  try:
41
41
  value = request.headers[header]
42
- except KeyError:
43
- raise MissingHeader(header)
42
+ except KeyError as exc:
43
+ raise MissingHeader(header) from exc
44
44
  try:
45
45
  return datetime.datetime.fromtimestamp(
46
46
  parse_http_date(value), tz=datetime.timezone.utc
@@ -60,8 +60,8 @@ def clean_tags(tags: Iterable[str]) -> Iterable[str]:
60
60
  def parse_header_etags(request: Request, header: str) -> FrozenSet[str]:
61
61
  try:
62
62
  parts = request.headers[header].split(",")
63
- except KeyError:
64
- raise MissingHeader(header)
63
+ except KeyError as exc:
64
+ raise MissingHeader(header) from exc
65
65
  tags = frozenset(clean_tags(parts))
66
66
  if not tags:
67
67
  raise InvalidHeader(header)
bananas/environment.py CHANGED
@@ -5,6 +5,7 @@ from typing import (
5
5
  Any,
6
6
  Callable,
7
7
  Dict,
8
+ Final,
8
9
  Generic,
9
10
  Iterable,
10
11
  List,
@@ -18,7 +19,7 @@ from typing import (
18
19
  )
19
20
 
20
21
  from django.conf import global_settings
21
- from typing_extensions import Final, Protocol, overload
22
+ from typing_extensions import Protocol, overload
22
23
 
23
24
  __all__ = ["env", "parse_bool", "parse_int", "parse_tuple", "parse_list", "parse_set"]
24
25
 
@@ -26,8 +27,7 @@ __all__ = ["env", "parse_bool", "parse_int", "parse_tuple", "parse_list", "parse
26
27
  log = logging.getLogger(__name__)
27
28
 
28
29
 
29
- class Undefined:
30
- ...
30
+ class Undefined: ...
31
31
 
32
32
 
33
33
  UNDEFINED: Final = Undefined()
@@ -94,12 +94,10 @@ Q = TypeVar("Q", covariant=True)
94
94
 
95
95
 
96
96
  class _Instantiable(Protocol[Q]):
97
- def __init__(self, value: Iterable[Q]) -> None:
98
- ...
97
+ def __init__(self, value: Iterable[Q]) -> None: ...
99
98
 
100
99
 
101
- class _InstantiableIterable(Iterable[Q], _Instantiable[Q], Generic[Q]):
102
- ...
100
+ class _InstantiableIterable(Iterable[Q], _Instantiable[Q], Generic[Q]): ...
103
101
 
104
102
 
105
103
  T = TypeVar("T", bound=_InstantiableIterable)
@@ -127,23 +125,19 @@ P = TypeVar("P", bound=Union[Builtin, tuple, list, set])
127
125
 
128
126
 
129
127
  @overload
130
- def get_parser(typ: Type[B]) -> Callable[[str], B]:
131
- ...
128
+ def get_parser(typ: Type[B]) -> Callable[[str], B]: ...
132
129
 
133
130
 
134
131
  @overload
135
- def get_parser(typ: Type[tuple]) -> Callable[[str], Tuple[str, ...]]:
136
- ...
132
+ def get_parser(typ: Type[tuple]) -> Callable[[str], Tuple[str, ...]]: ...
137
133
 
138
134
 
139
135
  @overload
140
- def get_parser(typ: Type[list]) -> Callable[[str], List[str]]:
141
- ...
136
+ def get_parser(typ: Type[list]) -> Callable[[str], List[str]]: ...
142
137
 
143
138
 
144
139
  @overload
145
- def get_parser(typ: Type[set]) -> Callable[[str], Set[str]]:
146
- ...
140
+ def get_parser(typ: Type[set]) -> Callable[[str], Set[str]]: ...
147
141
 
148
142
 
149
143
  def get_parser(typ: Type[P]) -> Callable[[str], P]:
@@ -165,8 +159,8 @@ def get_parser(typ: Type[P]) -> Callable[[str], P]:
165
159
  set: parse_set,
166
160
  }[typ],
167
161
  )
168
- except KeyError:
169
- raise NotImplementedError("Unsupported setting type: %r", typ)
162
+ except KeyError as exc:
163
+ raise NotImplementedError("Unsupported setting type: %r", typ) from exc
170
164
 
171
165
 
172
166
  def get_settings() -> Dict[str, Any]:
@@ -189,8 +183,8 @@ def get_settings() -> Dict[str, Any]:
189
183
  if key:
190
184
  if key in UNSUPPORTED_ENV_SETTINGS:
191
185
  raise ValueError(
192
- 'Django setting "{}" can not be '
193
- "configured through environment.".format(key)
186
+ f'Django setting "{key}" can not be '
187
+ "configured through environment."
194
188
  )
195
189
 
196
190
  default_value = getattr(global_settings, key, UNDEFINED)
@@ -201,9 +195,9 @@ def get_settings() -> Dict[str, Any]:
201
195
  parse = get_parser(SETTINGS_TYPES[key])
202
196
  else:
203
197
  # Determine parser by django setting type
204
- parse = get_parser(type(default_value))
198
+ parse = get_parser(type(default_value)) # type: ignore[type-var]
205
199
 
206
- value = parse(value)
200
+ value = parse(value) # type: ignore[assignment]
207
201
 
208
202
  settings[key] = value
209
203
 
@@ -237,12 +231,12 @@ class EnvironWrapper:
237
231
  get: Callable[..., str]
238
232
 
239
233
  @overload
240
- def parse(self, parser: Callable[[str], S], key: str, default: None) -> Optional[S]:
241
- ...
234
+ def parse(
235
+ self, parser: Callable[[str], S], key: str, default: None
236
+ ) -> Optional[S]: ...
242
237
 
243
238
  @overload
244
- def parse(self, parser: Callable[[str], S], key: str, default: S) -> S:
245
- ...
239
+ def parse(self, parser: Callable[[str], S], key: str, default: S) -> S: ...
246
240
 
247
241
  def parse(
248
242
  self, parser: Callable[[str], S], key: str, default: Optional[S] = None
@@ -253,64 +247,52 @@ class EnvironWrapper:
253
247
  try:
254
248
  return parser(value)
255
249
  except ValueError:
256
- log.warning(
257
- "Unable to parse environment variable {key}={value}".format(
258
- key=key, value=value
259
- )
260
- )
250
+ log.warning(f"Unable to parse environment variable {key}={value}")
261
251
  return default
262
252
 
263
253
  @overload
264
- def get_bool(self, key: str, default: U) -> Union[bool, U]:
265
- ...
254
+ def get_bool(self, key: str, default: U) -> Union[bool, U]: ...
266
255
 
267
256
  @overload
268
- def get_bool(self, key: str, default: None = None) -> Optional[bool]:
269
- ...
257
+ def get_bool(self, key: str, default: None = None) -> Optional[bool]: ...
270
258
 
271
259
  def get_bool(self, key: str, default: object = None) -> object:
272
260
  return self.parse(parse_bool, key, default=default)
273
261
 
274
262
  @overload
275
- def get_int(self, key: str, default: U) -> Union[int, U]:
276
- ...
263
+ def get_int(self, key: str, default: U) -> Union[int, U]: ...
277
264
 
278
265
  @overload
279
- def get_int(self, key: str, default: None = None) -> Optional[int]:
280
- ...
266
+ def get_int(self, key: str, default: None = None) -> Optional[int]: ...
281
267
 
282
268
  def get_int(self, key: str, default: object = None) -> object:
283
269
  return self.parse(parse_int, key, default=default)
284
270
 
285
271
  @overload
286
- def get_tuple(self, key: str, default: U) -> Union[Tuple[str, ...], U]:
287
- ...
272
+ def get_tuple(self, key: str, default: U) -> Union[Tuple[str, ...], U]: ...
288
273
 
289
274
  @overload
290
- def get_tuple(self, key: str, default: None = None) -> Optional[Tuple[str, ...]]:
291
- ...
275
+ def get_tuple(
276
+ self, key: str, default: None = None
277
+ ) -> Optional[Tuple[str, ...]]: ...
292
278
 
293
279
  def get_tuple(self, key: str, default: object = None) -> object:
294
280
  return self.parse(parse_tuple, key, default=default)
295
281
 
296
282
  @overload
297
- def get_list(self, key: str, default: U) -> Union[List[str], U]:
298
- ...
283
+ def get_list(self, key: str, default: U) -> Union[List[str], U]: ...
299
284
 
300
285
  @overload
301
- def get_list(self, key: str, default: None = None) -> Optional[List[str]]:
302
- ...
286
+ def get_list(self, key: str, default: None = None) -> Optional[List[str]]: ...
303
287
 
304
288
  def get_list(self, key: str, default: object = None) -> object:
305
289
  return self.parse(parse_list, key, default=default)
306
290
 
307
291
  @overload
308
- def get_set(self, key: str, default: U) -> Union[Set[str], U]:
309
- ...
292
+ def get_set(self, key: str, default: U) -> Union[Set[str], U]: ...
310
293
 
311
294
  @overload
312
- def get_set(self, key: str, default: None = None) -> Optional[Set[str]]:
313
- ...
295
+ def get_set(self, key: str, default: None = None) -> Optional[Set[str]]: ...
314
296
 
315
297
  def get_set(self, key: str, default: object = None) -> object:
316
298
  return self.parse(parse_set, key, default=default)
@@ -19,7 +19,7 @@ def collect_urls(
19
19
  pattern = urls.pattern.regex.pattern
20
20
  for x in urls.url_patterns:
21
21
  res += collect_urls(
22
- x, namespace=urls.namespace or namespace, prefix=prefix + [pattern]
22
+ x, namespace=urls.namespace or namespace, prefix=[*prefix, pattern]
23
23
  )
24
24
  return res
25
25
  elif isinstance(urls, URLPattern):
@@ -30,7 +30,7 @@ def collect_urls(
30
30
  [
31
31
  ("namespace", namespace),
32
32
  ("name", urls.name),
33
- ("pattern", prefix + [pattern]),
33
+ ("pattern", [*prefix, pattern]),
34
34
  ("lookup_str", lookup_str),
35
35
  ("default_args", dict(urls.default_args or {})),
36
36
  ]
@@ -3,7 +3,6 @@ from django.core.management.base import BaseCommand, CommandError
3
3
 
4
4
 
5
5
  class Command(BaseCommand):
6
-
7
6
  help = "Create admin permissions"
8
7
 
9
8
  def handle(self, *args: object, **options: object) -> None:
bananas/models.py CHANGED
@@ -4,23 +4,20 @@ import math
4
4
  import os
5
5
  import uuid
6
6
  from itertools import chain
7
- from typing import Any, Dict, Mapping, Optional, Sized
7
+ from typing import Any, ClassVar, Dict, Final, Mapping, Optional, Sized
8
8
 
9
9
  from django.core.exceptions import ValidationError
10
10
  from django.db import models
11
11
  from django.utils.translation import gettext_lazy as _
12
- from typing_extensions import Final
13
12
 
14
13
 
15
- class Missing:
16
- ...
14
+ class Missing: ...
17
15
 
18
16
 
19
17
  MISSING: Final = Missing()
20
18
 
21
19
 
22
20
  class ModelDict(Dict[str, Any]):
23
-
24
21
  _nested: Optional[Dict[str, "ModelDict"]] = None
25
22
 
26
23
  def __getattr__(self, item: str) -> Any:
@@ -120,9 +117,7 @@ class ModelDict(Dict[str, Any]):
120
117
  )
121
118
  else:
122
119
  raise AttributeError(
123
- "{!r} does not have {!r} attribute".format(
124
- previous_value, _field
125
- )
120
+ f"{previous_value!r} does not have {_field!r} attribute"
126
121
  )
127
122
 
128
123
  elif value is None:
@@ -180,9 +175,9 @@ class UUIDModel(models.Model):
180
175
  class SecretField(models.CharField):
181
176
  description = _("Generates and stores a random key.")
182
177
 
183
- default_error_messages = {
184
- "random-is-none": _("%(cls)s.get_random_bytes returned None"), # type: ignore[dict-item]
185
- "random-too-short": _( # type: ignore[dict-item]
178
+ default_error_messages = { # noqa: RUF012
179
+ "random-is-none": _("%(cls)s.get_random_bytes returned None"),
180
+ "random-too-short": _(
186
181
  "Too few random bytes received from "
187
182
  "get_random_bytes. Number of"
188
183
  " bytes=%(num_bytes)s,"
bananas/query.py CHANGED
@@ -38,12 +38,16 @@ class ModelDictIterable:
38
38
  query = queryset.query
39
39
  compiler = query.get_compiler(queryset.db)
40
40
 
41
- field_names: List[str] = list(query.values_select)
42
- extra_names: List[str] = list(query.extra_select)
43
- annotation_names: List[str] = list(query.annotation_select)
41
+ if hasattr(query, "selected") and query.selected:
42
+ names = list(query.selected)
43
+ else:
44
+ extra_names: List[str] = list(query.extra_select)
45
+ field_names: List[str] = list(query.values_select)
46
+ annotation_names: List[str] = list(query.annotation_select)
47
+
48
+ # Modified super(); rename fields given in queryset.values() kwargs
49
+ names = extra_names + field_names + annotation_names
44
50
 
45
- # Modified super(); rename fields given in queryset.values() kwargs
46
- names = extra_names + field_names + annotation_names
47
51
  if self.named_fields:
48
52
  names = self.rename_fields(names)
49
53
 
@@ -62,8 +66,7 @@ _MT_co = TypeVar("_MT_co", bound=Model, covariant=True)
62
66
  class IsQuerySet(Protocol[_MT_co]):
63
67
  def values(
64
68
  self, *fields: Union[str, Combinable], **expressions: Any
65
- ) -> "_QuerySet[_MT_co, ModelDict]":
66
- ...
69
+ ) -> "_QuerySet[_MT_co, ModelDict]": ...
67
70
 
68
71
 
69
72
  class ModelDictQuerySetMixin:
@@ -74,7 +77,7 @@ class ModelDictQuerySetMixin:
74
77
  fields += tuple(named_fields.values())
75
78
 
76
79
  clone = self.values(*fields)
77
- clone._iterable_class = ModelDictIterable # type: ignore[assignment]
80
+ clone._iterable_class = ModelDictIterable
78
81
 
79
82
  # QuerySet._hints is a dict object used by db router
80
83
  # to aid deciding which db should get a request. Currently
@@ -82,7 +85,7 @@ class ModelDictQuerySetMixin:
82
85
  # fine to set a custom key on this dict as it's a guaranteed
83
86
  # way that it'll be returned with the QuerySet instance
84
87
  # while leaving the queryset intact
85
- clone._add_hints(**{"_named_fields": named_fields}) # type: ignore[attr-defined]
88
+ clone._add_hints(**{"_named_fields": named_fields})
86
89
 
87
90
  return clone
88
91
 
@@ -92,7 +95,7 @@ _MT = TypeVar("_MT", bound=Model)
92
95
 
93
96
  if TYPE_CHECKING:
94
97
 
95
- class ModelDictQuerySet( # type: ignore[misc]
98
+ class ModelDictQuerySet(
96
99
  ModelDictQuerySetMixin,
97
100
  QuerySet[_MT],
98
101
  IsQuerySet[_MT],
bananas/secrets.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
- from typing import Optional
2
+ from typing import Final, Optional
3
3
 
4
- from typing_extensions import Final, overload
4
+ from typing_extensions import overload
5
5
 
6
6
  from .environment import env
7
7
 
@@ -9,13 +9,11 @@ BANANAS_SECRETS_DIR_ENV_KEY: Final = "BANANAS_SECRETS_DIR"
9
9
 
10
10
 
11
11
  @overload
12
- def get_secret(secret_name: str, default: str) -> str:
13
- ...
12
+ def get_secret(secret_name: str, default: str) -> str: ...
14
13
 
15
14
 
16
15
  @overload
17
- def get_secret(secret_name: str) -> Optional[str]:
18
- ...
16
+ def get_secret(secret_name: str) -> Optional[str]: ...
19
17
 
20
18
 
21
19
  def get_secret(secret_name: str, default: Optional[str] = None) -> Optional[str]:
@@ -75,6 +75,7 @@ body:not(.login) #header {
75
75
  }
76
76
 
77
77
  #branding {
78
+ width: 100%;
78
79
  float: none;
79
80
  }
80
81
 
@@ -113,6 +114,7 @@ body.login #branding a:active {
113
114
  }
114
115
 
115
116
  #branding a.logo {
117
+ width: 100%;
116
118
  padding-top: 15px;
117
119
  line-height: calc(70px - 15px * 2) /* fallback */;
118
120
  line-height: var(--topbar-height-inner);
@@ -144,8 +146,8 @@ body.login #branding a:active {
144
146
  height: 100%;
145
147
  }
146
148
  #header .searchable nav {
147
- height: calc(100% - 45px) /* fallback */;
148
- height: calc(100% - var(--searchbar-height));
149
+ height: calc(100% - 45px - 15px) /* fallback */;
150
+ height: calc(100% - var(--searchbar-height) - 15px);
149
151
  }
150
152
  #header nav > ul {
151
153
  overflow-y: auto;
@@ -171,7 +173,7 @@ body.login #branding a:active {
171
173
  margin-top: 15px;
172
174
  }
173
175
  #header .filtered-results nav > ul:first-of-type > li:first-of-type {
174
- margin-top: 30px;
176
+ margin-top: 15px;
175
177
  }
176
178
 
177
179
  #header nav > ul > li > a,
@@ -381,6 +383,11 @@ body.popup #title .back-arrow {
381
383
  background-color: transparent;
382
384
  }
383
385
 
386
+ #content h2:first-of-type {
387
+ font-size: 22px;
388
+ margin-top: 0;
389
+ }
390
+
384
391
  #content .object-tools {
385
392
  position: fixed;
386
393
  z-index: 3000;
@@ -527,6 +534,7 @@ input[type="button"]:disabled,
527
534
  .timelist a:hover,
528
535
  .timelist a:focus,
529
536
  .calendar td.selected a {
537
+ color: white;
530
538
  background-color: #417690 /* fallback */;
531
539
  background-color: var(--theme-color);
532
540
  background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.25)) /* fallback */;
@@ -572,6 +580,8 @@ textarea:disabled {
572
580
  background: none;
573
581
  color: #333;
574
582
  font-size: 22px;
583
+ border: 0;
584
+ text-transform: unset;
575
585
  }
576
586
 
577
587
  .module > h2 a.section:link,
@@ -604,26 +614,6 @@ a.active.selector-clearall:hover {
604
614
  /* TODO: selector-icons.svg is hardcoded blue on hover */
605
615
  }
606
616
 
607
- @supports (background-blend-mode: overlay) {
608
- .datetimeshortcuts .date-icon,
609
- .datetimeshortcuts .clock-icon {
610
- background: none;
611
- background-color: #447e9b /* fallback */;
612
- background-color: var(--secondary-color);
613
- background-blend-mode: overlay;
614
- }
615
-
616
- .datetimeshortcuts .date-icon {
617
- -webkit-mask-image: url(../../img/icon-calendar.svg);
618
- mask-image: url(../../img/icon-calendar.svg);
619
- }
620
-
621
- .datetimeshortcuts .clock-icon {
622
- -webkit-mask-image: url(../../img/icon-clock.svg);
623
- mask-image: url(../../img/icon-clock.svg);
624
- }
625
- }
626
-
627
617
  .calendar caption,
628
618
  .calendarbox h2 {
629
619
  color: white;
@@ -634,12 +624,15 @@ a.active.selector-clearall:hover {
634
624
  filter: invert(1);
635
625
  }
636
626
 
637
- .calendarbox .calendarnav-previous {
638
- background-position: 0 -15px;
627
+ .calendar-cancel a {
628
+ color: var(--button-fg);
639
629
  }
640
630
 
641
- .calendarbox .calendarnav-next {
642
- background-position: 0 -45px;
631
+ .clockbox h2 {
632
+ font-size: 1.1em;
633
+ color: white;
634
+ background: var(--theme-color);
635
+ background-image: var(--bg-lighten-25);
643
636
  }
644
637
 
645
638
  #header #changelist-filter,
@@ -685,6 +678,21 @@ a.active.selector-clearall:hover {
685
678
  margin: 0;
686
679
  }
687
680
 
681
+ #header #changelist-filter h3 .viewlink {
682
+ background: none;
683
+ }
684
+ #header #changelist-filter h3 .viewlink:hover {
685
+ color: var(--lighten-75);
686
+ }
687
+
688
+ #changelist-filter #changelist-filter-extra-actions {
689
+ border-bottom: 0;
690
+ }
691
+
692
+ #changelist-filter details[open] > summary::before {
693
+ color: var(--lighten-25);
694
+ }
695
+
688
696
  #header #object-tools h2 {
689
697
  margin: 0;
690
698
  }
@@ -735,6 +743,11 @@ body:not(.popup) #changelist #toolbar {
735
743
  margin-right: 0;
736
744
  }
737
745
 
746
+ .nav-global #toolbar {
747
+ border: 0;
748
+ margin-bottom: 25px;
749
+ }
750
+
738
751
  .nav-global #toolbar form {
739
752
  display: block;
740
753
  }
@@ -764,7 +777,9 @@ body:not(.popup) #changelist #toolbar {
764
777
  background-image: var(--bg-lighten-33);
765
778
  color: #fff;
766
779
  line-height: 25px;
767
- height: 25px;
780
+ height: 45px;
781
+ padding: 10px 0;
782
+ box-sizing: border-box;
768
783
  }
769
784
 
770
785
  #changelist .results ~ .actions {
@@ -778,6 +793,7 @@ body:not(.popup) #changelist #toolbar {
778
793
  }
779
794
 
780
795
  #changelist .actions label {
796
+ padding-left: 10px;
781
797
  vertical-align: top;
782
798
  }
783
799
 
@@ -806,3 +822,7 @@ body:not(.popup) #changelist #toolbar {
806
822
  #changelist .actions span.question {
807
823
  margin: 0 0 0 10px;
808
824
  }
825
+
826
+ .selector-chosen-title {
827
+ background: var(--theme-color);
828
+ }
@@ -48,9 +48,8 @@ body:not(.popup) #header #object-tools {
48
48
  padding: 0;
49
49
  border-radius: 0;
50
50
  border: 0;
51
- background: #417690 /* fallback */;
52
- background: var(--theme-color);
53
- background-image: url(../../img/search.svg), linear-gradient(to bottom, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15));
51
+ background: white;
52
+ background-image: url(../../img/search.svg);
54
53
  background-position: center center;
55
54
  background-repeat: no-repeat;
56
55
  box-shadow: none;
@@ -58,7 +57,7 @@ body:not(.popup) #header #object-tools {
58
57
  }
59
58
 
60
59
  #header #changelist-search input[type="submit"]:hover {
61
- background-image: url(../../img/search.svg), linear-gradient(to bottom, rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.35));
60
+ background-image: url(../../img/search.svg), linear-gradient(to bottom, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15));
62
61
  }
63
62
 
64
63
  #changelist-search > div {
@@ -123,6 +122,12 @@ body:not(.popup) #header #object-tools {
123
122
  transform: translateX(var(--sidebar-width));
124
123
  }
125
124
 
125
+ html.is-sidebarOpen #header nav {
126
+ position: unset;
127
+ width: var(--sidebar-width);
128
+ transform: translateY(-15px);
129
+ }
130
+
126
131
  body:not(.popup) #content .object-tools {
127
132
  display: none;
128
133
  }
@@ -140,6 +145,14 @@ body:not(.popup) #header #object-tools {
140
145
  margin-right: 0;
141
146
  }
142
147
 
148
+ #changelist > .changelist-form-container {
149
+ max-width: 100vw;
150
+ }
151
+
152
+ html.is-sidebarOpen #changelist > .changelist-form-container {
153
+ max-width: calc(100vw - var(--sidebar-width));
154
+ }
155
+
143
156
  #changelist .actions {
144
157
  padding: 10px;
145
158
  left: 0;
@@ -120,7 +120,10 @@
120
120
  {% endif %}
121
121
 
122
122
  <li>
123
- <a href="{% url 'admin:logout' %}">
123
+ <form id="logout-form" method="post" action="{% url 'admin:logout' %}">
124
+ {% csrf_token %}
125
+ </form>
126
+ <a href="" onclick="document.getElementById('logout-form').submit();">
124
127
  {% trans 'Log out' %}
125
128
  </a>
126
129
  </li>
bananas/url.py CHANGED
@@ -27,10 +27,9 @@ Currently supported engines are:
27
27
  You can add your own by running ``register(scheme, module_name)`` before
28
28
  parsing.
29
29
  """
30
- from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union
31
- from urllib.parse import parse_qs, unquote_plus, urlsplit
32
30
 
33
- from typing_extensions import Final
31
+ from typing import Any, Dict, Final, List, Mapping, NamedTuple, Optional, Tuple, Union
32
+ from urllib.parse import parse_qs, unquote_plus, urlsplit
34
33
 
35
34
 
36
35
  class Alias:
@@ -97,8 +96,8 @@ def resolve(
97
96
  ), "Multiple levels of aliases are not supported"
98
97
 
99
98
  return result
100
- except KeyError:
101
- raise KeyError("No matches for engine %s" % key)
99
+ except KeyError as exc:
100
+ raise KeyError("No matches for engine %s" % key) from exc
102
101
 
103
102
 
104
103
  def get_engine(scheme: str) -> str:
@@ -128,12 +127,12 @@ def get_engine(scheme: str) -> str:
128
127
 
129
128
  try:
130
129
  engine, extra = engine
131
- except ValueError:
130
+ except ValueError as exc:
132
131
  # engine was not a list of length 2
133
132
  raise ValueError(
134
133
  "django-bananas.url' engine "
135
134
  "configuration is invalid: %r" % ENGINE_MAPPING
136
- )
135
+ ) from exc
137
136
 
138
137
  assert isinstance(
139
138
  extra, Mapping
@@ -1,17 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-bananas
3
- Version: 2.2
3
+ Version: 2.4
4
4
  Summary: Django Bananas - Django extensions the monkey way
5
5
  Home-page: https://github.com/5monkeys/django-bananas
6
6
  License: MIT License
7
7
  Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Programming Language :: Python
9
9
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.6
11
- Classifier: Programming Language :: Python :: 3.7
12
10
  Classifier: Programming Language :: Python :: 3.8
13
11
  Classifier: Programming Language :: Python :: 3.9
14
12
  Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Framework :: Django
17
+ Classifier: Framework :: Django :: 3.2
18
+ Classifier: Framework :: Django :: 4.0
19
+ Classifier: Framework :: Django :: 4.1
20
+ Classifier: Framework :: Django :: 4.2
21
+ Classifier: Framework :: Django :: 5.0
22
+ Classifier: Framework :: Django :: 5.1
23
+ Classifier: Framework :: Django :: 5.2
15
24
  Classifier: Intended Audience :: Developers
16
25
  Classifier: License :: OSI Approved :: MIT License
17
26
  Classifier: Operating System :: OS Independent
@@ -19,21 +28,24 @@ Classifier: Framework :: Django
19
28
  Requires-Python: >=3.6
20
29
  Description-Content-Type: text/x-rst; charset=UTF-8
21
30
  License-File: LICENSE
22
- Requires-Dist: Django (>=2.2)
23
- Requires-Dist: typing-extensions (>=3.7.4.3)
24
- Requires-Dist: drf-yasg (>=1.20.0)
25
- Provides-Extra: dev
26
- Requires-Dist: mypy ; extra == 'dev'
27
- Requires-Dist: django-stubs ; extra == 'dev'
28
- Requires-Dist: djangorestframework-stubs ; extra == 'dev'
29
- Requires-Dist: pytest-mypy-plugins ; extra == 'dev'
30
- Requires-Dist: pre-commit ; extra == 'dev'
31
+ Requires-Dist: Django>=2.2
32
+ Requires-Dist: typing-extensions>=3.7.4.3
31
33
  Provides-Extra: drf
32
- Requires-Dist: djangorestframework (>=3.10) ; extra == 'drf'
33
- Requires-Dist: drf-yasg (>=1.20.0) ; extra == 'drf'
34
+ Requires-Dist: djangorestframework>=3.10; extra == "drf"
35
+ Requires-Dist: drf-yasg>=1.20.0; extra == "drf"
34
36
  Provides-Extra: test
35
- Requires-Dist: tox ; extra == 'test'
36
- Requires-Dist: coverage ; extra == 'test'
37
+ Requires-Dist: tox; extra == "test"
38
+ Requires-Dist: coverage[toml]; extra == "test"
39
+ Provides-Extra: dev
40
+ Requires-Dist: mypy; extra == "dev"
41
+ Requires-Dist: types-setuptools; extra == "dev"
42
+ Requires-Dist: django-stubs; extra == "dev"
43
+ Requires-Dist: djangorestframework-stubs; extra == "dev"
44
+ Requires-Dist: pytest; extra == "dev"
45
+ Requires-Dist: pytest-cov; extra == "dev"
46
+ Requires-Dist: pytest-django; extra == "dev"
47
+ Requires-Dist: pre-commit; extra == "dev"
48
+ Dynamic: license-file
37
49
 
38
50
  ================================================================================
39
51
  :banana: Django Bananas - Django extensions the monkey way
@@ -69,10 +81,13 @@ with the ``drf`` extra to keep those in sync:
69
81
 
70
82
  Currently tested only for
71
83
 
72
- - Django 3.2 under Python 3.7-3.9
84
+ - Django 3.2 under Python 3.8-3.10
73
85
  - Django 4.0 under Python 3.8-3.10
74
- - Django 4.1 under Python 3.8-3.10
75
- - Django 4.2 under Python 3.8-3.10
86
+ - Django 4.1 under Python 3.8-3.13
87
+ - Django 4.2 under Python 3.8-3.13
88
+ - Django 5.0 under Python 3.10-3.13
89
+ - Django 5.1 under Python 3.10-3.13
90
+ - Django 5.2 under Python 3.10-3.13
76
91
 
77
92
  Pull requests welcome!
78
93
 
@@ -275,7 +290,6 @@ feature requires installation with the ``drf`` extra.
275
290
 
276
291
 
277
292
  class CustomAdminAPI(BananasAdminAPI):
278
-
279
293
  name = lazy_title(_("custom"))
280
294
 
281
295
  @schema(query_serializer=SomeSerializer, responses={200: SomeSerializer})
@@ -284,7 +298,6 @@ feature requires installation with the ``drf`` extra.
284
298
 
285
299
 
286
300
  class SomeModelAdminAPI(BananasAPI, viewsets.ModelViewSet):
287
-
288
301
  serializer_class = SomeModelSerializer
289
302
 
290
303
  def list(self, request):
@@ -593,4 +606,4 @@ and select specific tests with the ``test`` argument to ``make test``:
593
606
 
594
607
  .. code-block:: bash
595
608
 
596
- make test test='tests.test_admin.APITest.test_logout'
609
+ make test test='-k test_logout'
@@ -0,0 +1,47 @@
1
+ bananas/__init__.py,sha256=HBTRN6OCuRJ35rOq9Scw0Q_tH_VrxXXcWvRjUDjLtpc,686
2
+ bananas/environment.py,sha256=1UwUNjSbnLlWh0XN41_AdEc0TVXQbbswkM34KNPzNwk,7744
3
+ bananas/lazy.py,sha256=XFaqACJ6K5s_MZcxK_5OLQU35F7c7jB3KsdAtHuYQ6k,140
4
+ bananas/models.py,sha256=upapLi3Zvk073G4oMXCobh7RRXDhBLQ0kjPTWWAeYCM,8797
5
+ bananas/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ bananas/query.py,sha256=vDvbY3_KSri_bUZflmUDb2zo46kuZJpuWVBL6s2lX4Y,3615
7
+ bananas/secrets.py,sha256=WDBZapuPzDGHUwO1J_UjFuoljFkgbLKYy0uFTIl53Fk,1021
8
+ bananas/settings.py,sha256=HGZBxm5BMwHJc9PpQpvkzAuasXvePa-q_4tgeup9DPA,139
9
+ bananas/url.py,sha256=63vR_bK0Py9X-8dA7seEPVrQaU8FRSIOLwUb8XIUDAk,7907
10
+ bananas/admin/__init__.py,sha256=0Dzu51GjYjbpnNf5ByaKTx5xiYZ0vxfhW1EssFuiQ90,173
11
+ bananas/admin/extension.py,sha256=pfAjvKrGpnHZoX5YQSXM9-6GojFvLtUkwgN9OhHIekI,13632
12
+ bananas/admin/i18n.py,sha256=WxK_LKnPjoFgoNiNuZdr5wk3LoMXdu7acaIsZNsOxc0,301
13
+ bananas/admin/api/__init__.py,sha256=86dXxHQKnQk-2ZOT2znTBasX-k7nPKlxslwYVZlzu4g,54
14
+ bananas/admin/api/mixins.py,sha256=NroiouccAD6MORoFdS-I3Xnjsmm69_JhdQRT5Roe240,5287
15
+ bananas/admin/api/permissions.py,sha256=sIIHfuJERYvdSJQ8YwNV4W3tj1DPlICi1snAe5RGq2g,375
16
+ bananas/admin/api/router.py,sha256=VWn6O97sbsoPZtKPLPxPDn_raO7Gy8B62N1s7-mGHhU,570
17
+ bananas/admin/api/serializers.py,sha256=XziZ1-6h4hn1qgts3qB8W18ctpl6h8vP3dkqa7WxLe8,2964
18
+ bananas/admin/api/urls.py,sha256=fch3C5BhI6d12U9un8I28fFiHZWTWFH5XAhAzXPURxY,398
19
+ bananas/admin/api/versioning.py,sha256=ciyelTzGZtJDPnigRZAJ0Gk1ZHfGxM9FXctS-OoyJ4Y,901
20
+ bananas/admin/api/views.py,sha256=at_iF0Q860srlsyqjuQq9D274PItTSrXp0uURgegqrE,4169
21
+ bananas/admin/api/schemas/__init__.py,sha256=n4vZ3Sd1H23krxIIUWvktHimmdfh1n0K-J4z2-QsZxI,336
22
+ bananas/admin/api/schemas/base.py,sha256=XFfSQE_VeUraB_LIge1xEtVYOaTGfx6aDq6eahd6D2o,449
23
+ bananas/admin/api/schemas/decorators.py,sha256=Zwp7AwV_6Y0iWVcsO8VjNHHttYMcdGnrfr7ElwEzJzE,492
24
+ bananas/admin/api/schemas/yasg.py,sha256=FpdsUSeuOuH_Rgm_C0H4Q3Tb6rG3McvJ7B43R1ALfbw,6036
25
+ bananas/admin/api/v1_0/__init__.py,sha256=1M909E2Ei8-GI0Xox9t5NfO3xpPG2F13znjlx_jXGZc,59
26
+ bananas/admin/api/v1_0/urls.py,sha256=8N-fFqycB-SnzTMQQk-TTM1b7GuRGXlPtnRFyv6J2rk,695
27
+ bananas/drf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ bananas/drf/errors.py,sha256=JDZYblAio27rh-nAOtC_4CoVw_lFYIdmmd4bTrgxaQc,427
29
+ bananas/drf/fencing.py,sha256=XyCNiPqx0eKMk1emjp7kvyvj7P9JvDArIWgKeQrCRD4,7264
30
+ bananas/drf/utils.py,sha256=Cg7oBNNPuFwIHwHldSk3za87oesK6sivCbiRWP4zj0s,1824
31
+ bananas/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ bananas/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ bananas/management/commands/show_urls.py,sha256=X6ipP04XN1pdrT2Ne5ozbMx068SQ8Io-HpO6Xi7v7d0,2220
34
+ bananas/management/commands/syncpermissions.py,sha256=fmsJpikAPAwvK8Yfkj4A53fdUOz-y0rHEF7xsRiy8W0,1565
35
+ bananas/static/admin/bananas/css/bananas.css,sha256=h7IuijjLYePQ5vwMWNP_Piyxn7IDBMWBM5Of0n6knNE,17882
36
+ bananas/static/admin/bananas/css/banansive.css,sha256=P2EuItR7n0l_hn3X0mIMKhj4cQc4hnjg4PvQjyknLBg,3984
37
+ bananas/static/admin/bananas/img/django.svg,sha256=4S1Cjntojkcoj17oTNufVtnlflhoKsRuQ9O7hTqyx6Q,5291
38
+ bananas/static/admin/bananas/img/search.svg,sha256=59UwyGYVJ4EJ26d5AAIdjtCHR3CnO94in6eQIbkYTu8,458
39
+ bananas/static/admin/bananas/js/bananas.js,sha256=1Xv6JEiTZYeaJSTxkKyMLwIRJ40SnRDPi2N-prpWoU0,4596
40
+ bananas/static/admin/css/responsive.css,sha256=UAMABM7h5rRZ8FocJw4bfx36Q5XtIDm4ddXN8lMC4SQ,17894
41
+ bananas/templates/admin/base_site.html,sha256=RU67gxvx1ozx3-uCyXWf2jC9H6c4adm0BjwmKF8EHEo,7977
42
+ bananas/templates/admin/view.html,sha256=WezwD9qGPch5S29intLVzbY1zVRop4zdIdm4pf7usVw,114
43
+ django_bananas-2.4.dist-info/licenses/LICENSE,sha256=ezKzVU8KUSkiuP6I5I1PEfOUn33aerXQ_fU8_4Qavos,1081
44
+ django_bananas-2.4.dist-info/METADATA,sha256=13IP-MgvJRUUqgKRwtv6Lag1f5vhsq4bnzhfCPj2UCM,18680
45
+ django_bananas-2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ django_bananas-2.4.dist-info/top_level.txt,sha256=5JKVcC99qGcHeOm5WigbcrQbMcsWr6N2A0tF9ZoY6j8,8
47
+ django_bananas-2.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2014-2021 5 Monkeys
3
+ Copyright (c) 2014-2025 5 Monkeys
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,47 +0,0 @@
1
- bananas/__init__.py,sha256=WG_GvpIljTnCx45le_2Xq_hIeHEheJKdVd8UwWpyv6U,686
2
- bananas/environment.py,sha256=oBxSsFEMeTsizpe_qB3kkTOIvkxqImmRiv1WhZGHw4I,7874
3
- bananas/lazy.py,sha256=XFaqACJ6K5s_MZcxK_5OLQU35F7c7jB3KsdAtHuYQ6k,140
4
- bananas/models.py,sha256=Fd__soRYoyPEaClO7vmdIDzx78mIrtOnpfJKTlyrtNw,8931
5
- bananas/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- bananas/query.py,sha256=ZtLtJZZVt5_JUDaFxkstpwaHNnIcjfDM98EmCOCuzJY,3569
7
- bananas/secrets.py,sha256=JlTM2mecnvwRmUANTbxMgtyTnRoW3TCrb5uY9KFuW24,1029
8
- bananas/settings.py,sha256=HGZBxm5BMwHJc9PpQpvkzAuasXvePa-q_4tgeup9DPA,139
9
- bananas/url.py,sha256=SlnGoHRGnh4j6lvGuNniVz7U5eFYMyDlN_9PWvQX9mQ,7904
10
- bananas/admin/__init__.py,sha256=0Dzu51GjYjbpnNf5ByaKTx5xiYZ0vxfhW1EssFuiQ90,173
11
- bananas/admin/extension.py,sha256=6t2rNN_NRcIS6sveuGhmyKSW2UtBOlz8PhVe3V_QM4k,13664
12
- bananas/admin/api/__init__.py,sha256=86dXxHQKnQk-2ZOT2znTBasX-k7nPKlxslwYVZlzu4g,54
13
- bananas/admin/api/i18n.py,sha256=ZcZj3PeP2q4O5uZisK7Qk9TMVA6MQKCE2RKMZSwFHCU,302
14
- bananas/admin/api/mixins.py,sha256=F2-DPkVCqgtqMNZT8jeP_ODm5TjhgzSUKmDd046yicA,5350
15
- bananas/admin/api/permissions.py,sha256=sIIHfuJERYvdSJQ8YwNV4W3tj1DPlICi1snAe5RGq2g,375
16
- bananas/admin/api/router.py,sha256=VWn6O97sbsoPZtKPLPxPDn_raO7Gy8B62N1s7-mGHhU,570
17
- bananas/admin/api/serializers.py,sha256=XziZ1-6h4hn1qgts3qB8W18ctpl6h8vP3dkqa7WxLe8,2964
18
- bananas/admin/api/urls.py,sha256=fch3C5BhI6d12U9un8I28fFiHZWTWFH5XAhAzXPURxY,398
19
- bananas/admin/api/versioning.py,sha256=ATCoCf1uuY69lse-c0qNaBUOrQGIadAMk16UslyeUgo,882
20
- bananas/admin/api/views.py,sha256=9EYe1VdcF9NjPUSSElMxQ_9rDjOaYIDmzk7swQ02S1I,4083
21
- bananas/admin/api/schemas/__init__.py,sha256=n4vZ3Sd1H23krxIIUWvktHimmdfh1n0K-J4z2-QsZxI,336
22
- bananas/admin/api/schemas/base.py,sha256=EjYWpLmb1q1y2qbkhNcAFaiq-acT07PsVn89IKAIdp8,433
23
- bananas/admin/api/schemas/decorators.py,sha256=Zwp7AwV_6Y0iWVcsO8VjNHHttYMcdGnrfr7ElwEzJzE,492
24
- bananas/admin/api/schemas/yasg.py,sha256=Af5QKnc-UUMP6AqdGBgcVyfERw_70WnNs-Ho1LUCUzc,6019
25
- bananas/admin/api/v1_0/__init__.py,sha256=1M909E2Ei8-GI0Xox9t5NfO3xpPG2F13znjlx_jXGZc,59
26
- bananas/admin/api/v1_0/urls.py,sha256=nfH-6G3qzg3e0AJXxZLfEV0zyXqkNh8fJw_Tb5ALDpg,664
27
- bananas/drf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- bananas/drf/errors.py,sha256=JDZYblAio27rh-nAOtC_4CoVw_lFYIdmmd4bTrgxaQc,427
29
- bananas/drf/fencing.py,sha256=WJ_Hw5XgI3XrianquYO2ggtAB9tyRFqeCqm_7-0OTzM,7159
30
- bananas/drf/utils.py,sha256=cHLKsWl_yjpR3lO0gPvapKXg9M5_JpJEKi5wcswpwJg,1792
31
- bananas/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- bananas/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- bananas/management/commands/show_urls.py,sha256=Ng63v7YM5tw1M00mzJc6xdlcCS0w7NMg2TFG1djLGRU,2220
34
- bananas/management/commands/syncpermissions.py,sha256=WtVzNrEQl1tCVQKq9pPW1QwcIftbrwbhmN5WMPayR8k,1566
35
- bananas/static/admin/bananas/css/bananas.css,sha256=5WOuhE-vhrqKaXuwat56DTq7MPb79q7ajym0Nx-3vcU,17740
36
- bananas/static/admin/bananas/css/banansive.css,sha256=u9aYmL_ccDO9rDevOtckqhERDNs0HSazQ-4HJ9TukXI,3783
37
- bananas/static/admin/bananas/img/django.svg,sha256=4S1Cjntojkcoj17oTNufVtnlflhoKsRuQ9O7hTqyx6Q,5291
38
- bananas/static/admin/bananas/img/search.svg,sha256=59UwyGYVJ4EJ26d5AAIdjtCHR3CnO94in6eQIbkYTu8,458
39
- bananas/static/admin/bananas/js/bananas.js,sha256=1Xv6JEiTZYeaJSTxkKyMLwIRJ40SnRDPi2N-prpWoU0,4596
40
- bananas/static/admin/css/responsive.css,sha256=UAMABM7h5rRZ8FocJw4bfx36Q5XtIDm4ddXN8lMC4SQ,17894
41
- bananas/templates/admin/base_site.html,sha256=Hq4n99zOuGnzsHl1v1xmYI6uFKk1oaDVcY_bsaMFlMA,7793
42
- bananas/templates/admin/view.html,sha256=WezwD9qGPch5S29intLVzbY1zVRop4zdIdm4pf7usVw,114
43
- django_bananas-2.2.dist-info/LICENSE,sha256=bsI-BPMDuol2ZykVXKKQiwufT0JexcSLsU2Nlx3D-zc,1081
44
- django_bananas-2.2.dist-info/METADATA,sha256=AdpRMTzNJuPL9dqY_ggwG1Qwns4Bq50xC4v2FrcngQ0,18137
45
- django_bananas-2.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
46
- django_bananas-2.2.dist-info/top_level.txt,sha256=5JKVcC99qGcHeOm5WigbcrQbMcsWr6N2A0tF9ZoY6j8,8
47
- django_bananas-2.2.dist-info/RECORD,,