-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathutils.py
More file actions
209 lines (169 loc) · 6.6 KB
/
utils.py
File metadata and controls
209 lines (169 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""Utility classes and functions."""
from typing import TYPE_CHECKING, override
from asgiref.sync import sync_to_async
from django.core.exceptions import FieldDoesNotExist
from django.core.validators import RegexValidator
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from collections.abc import Set as AbstractSet
from typing import ClassVar, Final
from django.db.models.base import ModelBase
__all__: "Sequence[str]" = ("AsyncBaseModel", "DiscordMember")
class AsyncBaseModel(models.Model):
"""
Asynchronous base model, defining extra synchronous and asynchronous utility methods.
This class is abstract so should not be instantiated or have a table made for it in the
database (see https://docs.djangoproject.com/en/stable/topics/db/models#abstract-base-classes).
"""
INSTANCES_NAME_PLURAL: str
class Meta(TypedModelMeta): # noqa: D106
abstract: "ClassVar[bool]" = True
@override
def __init__(self, *args: object, **kwargs: object) -> None: # noqa: CAR150
proxy_fields: dict[str, object] = {
field_name: kwargs.pop(field_name)
for field_name in set(kwargs.keys()) & self._get_proxy_field_names()
}
with transaction.atomic():
super().__init__(*args, **kwargs) # noqa: CAR151
field_name: str
value: object
for field_name, value in proxy_fields.items():
setattr(self, field_name, value)
@override
def save(
self,
*,
force_insert: bool | tuple["ModelBase", ...] = False,
force_update: bool = False,
using: str | None = None,
update_fields: "Iterable[str] | None" = None,
) -> None:
self.full_clean()
return super().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
def update(
self,
*,
commit: bool = True,
force_insert: bool | tuple["ModelBase", ...] = False,
force_update: bool = False,
using: str | None = None,
update_fields: "Iterable[str] | None" = None,
**kwargs: object, # noqa: CAR150
) -> None:
"""
Change an in-memory object's values, then save it to the database.
This simplifies the two steps into a single operation
(based on Django's Queryset.bulk_update method).
The 'force_insert' and 'force_update' parameters can be used
to insist that the "save" must be an SQL insert or update
(or equivalent for non-SQL backends), respectively.
Normally, they should not be set.
"""
unexpected_kwargs: set[str] = set()
field_name: str
for field_name in set(kwargs.keys()) - self._get_proxy_field_names():
try:
self._meta.get_field(field_name)
except FieldDoesNotExist:
unexpected_kwargs.add(field_name)
if unexpected_kwargs:
UNEXPECTED_KWARGS_MESSAGE: Final[str] = (
f"{self._meta.model.__name__} got unexpected keyword arguments: "
f"{tuple(unexpected_kwargs)}"
)
raise TypeError(UNEXPECTED_KWARGS_MESSAGE)
with transaction.atomic():
value: object
for field_name, value in kwargs.items():
setattr(self, field_name, value)
if commit:
return self.save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
return None
setattr(update, "alters_data", True) # noqa: B010
async def aupdate(
self,
*,
commit: bool = True,
force_insert: bool | tuple["ModelBase", ...] = False,
force_update: bool = False,
using: str | None = None,
update_fields: "Iterable[str] | None" = None,
**kwargs: object, # noqa: CAR150
) -> None:
"""
Asynchronously change an in-memory object's values, then save it to the database.
This simplifies the two steps into a single operation
(based on Django's Queryset.bulk_update method).
The 'force_insert' and 'force_update' parameters can be used
to insist that the "save" must be an SQL insert or update
(or equivalent for non-SQL backends), respectively.
Normally, they should not be set.
"""
await sync_to_async(self.update)(
commit=commit,
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
**kwargs,
)
setattr(aupdate, "alters_data", True) # noqa: B010
@classmethod
def _get_proxy_field_names(cls) -> "AbstractSet[str]":
"""
Return the set of extra names of properties that can be saved to the database.
These are proxy fields because their values are not stored as object attributes,
however, they can be used as a reference to a real attribute when saving objects to the
database.
"""
return set()
class DiscordMember(AsyncBaseModel):
"""
Common model to represent a Discord guild member.
Instances of this model are related to other models to store information
about reminders, opt-in/out states, tracked committee actions, etc.
The Discord guild member is identified by their Discord member ID.
"""
discord_id = models.CharField(
_("Discord Member ID"),
unique=True,
null=False,
blank=False,
max_length=20,
validators=(
RegexValidator(
r"\A\d{17,20}\Z",
"discord_id must be a valid Discord member ID (see https://docs.pycord.dev/en/stable/api/abcs.html#discord.abc.Snowflake.id)",
),
),
)
@override
def __str__(self) -> str:
return self.discord_id
@override
def __repr__(self) -> str:
return f"<{self._meta.verbose_name}: {self.discord_id!r}>"
@property
def member_id(self) -> str: # noqa: D102
return self.discord_id
@member_id.setter
def member_id(self, value: str | int) -> None:
self.discord_id = str(value)
@classmethod
@override
def _get_proxy_field_names(cls) -> "AbstractSet[str]":
return {*super()._get_proxy_field_names(), "member_id"}