slidge 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +107 -40
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -804
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.3.dist-info/RECORD +0 -96
  100. slidge-0.1.3.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
slidge/db/models.py ADDED
@@ -0,0 +1,405 @@
1
+ import warnings
2
+ from datetime import datetime
3
+ from enum import IntEnum
4
+ from typing import Optional
5
+
6
+ import sqlalchemy as sa
7
+ from slixmpp import JID
8
+ from slixmpp.types import MucAffiliation, MucRole
9
+ from sqlalchemy import ForeignKey, Index, UniqueConstraint
10
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
11
+
12
+ from ..util.types import ClientType, MucType
13
+ from .meta import Base, JSONSerializable, JSONSerializableTypes
14
+
15
+
16
+ class XmppToLegacyEnum(IntEnum):
17
+ """
18
+ XMPP-client generated IDs, used in the XmppToLegacyIds table to keep track
19
+ of corresponding legacy IDs
20
+ """
21
+
22
+ DM = 1
23
+ GROUP_CHAT = 2
24
+ THREAD = 3
25
+
26
+
27
+ class ArchivedMessageSource(IntEnum):
28
+ """
29
+ Whether an archived message comes from ``LegacyMUC.backfill()`` or was received
30
+ as a "live" message.
31
+ """
32
+
33
+ LIVE = 1
34
+ BACKFILL = 2
35
+
36
+
37
+ class GatewayUser(Base):
38
+ """
39
+ A user, registered to the gateway component.
40
+ """
41
+
42
+ __tablename__ = "user_account"
43
+ id: Mapped[int] = mapped_column(primary_key=True)
44
+ jid: Mapped[JID] = mapped_column(unique=True)
45
+ registration_date: Mapped[datetime] = mapped_column(
46
+ sa.DateTime, server_default=sa.func.now()
47
+ )
48
+
49
+ legacy_module_data: Mapped[JSONSerializable] = mapped_column(default={})
50
+ """
51
+ Arbitrary non-relational data that legacy modules can use
52
+ """
53
+ preferences: Mapped[JSONSerializable] = mapped_column(default={})
54
+ avatar_hash: Mapped[Optional[str]] = mapped_column(default=None)
55
+ """
56
+ Hash of the user's avatar, to avoid re-publishing the same avatar on the
57
+ legacy network
58
+ """
59
+
60
+ contacts: Mapped[list["Contact"]] = relationship(
61
+ back_populates="user", cascade="all, delete-orphan"
62
+ )
63
+ rooms: Mapped[list["Room"]] = relationship(
64
+ back_populates="user", cascade="all, delete-orphan"
65
+ )
66
+ xmpp_to_legacy: Mapped[list["XmppToLegacyIds"]] = relationship(
67
+ cascade="all, delete-orphan"
68
+ )
69
+ attachments: Mapped[list["Attachment"]] = relationship(cascade="all, delete-orphan")
70
+ multi_legacy: Mapped[list["LegacyIdsMulti"]] = relationship(
71
+ cascade="all, delete-orphan"
72
+ )
73
+ multi_xmpp: Mapped[list["XmppIdsMulti"]] = relationship(
74
+ cascade="all, delete-orphan"
75
+ )
76
+
77
+ def __repr__(self) -> str:
78
+ return f"User(id={self.id!r}, jid={self.jid!r})"
79
+
80
+ def get(self, field: str, default: str = "") -> JSONSerializableTypes:
81
+ # """
82
+ # Get fields from the registration form (required to comply with slixmpp backend protocol)
83
+ #
84
+ # :param field: Name of the field
85
+ # :param default: Default value to return if the field is not present
86
+ #
87
+ # :return: Value of the field
88
+ # """
89
+ return self.legacy_module_data.get(field, default)
90
+
91
+ @property
92
+ def registration_form(self) -> dict:
93
+ # Kept for retrocompat, should be
94
+ # FIXME: delete me
95
+ warnings.warn(
96
+ "GatewayUser.registration_form is deprecated.", DeprecationWarning
97
+ )
98
+ return self.legacy_module_data
99
+
100
+
101
+ class Avatar(Base):
102
+ """
103
+ Avatars of contacts, rooms and participants.
104
+
105
+ To comply with XEPs, we convert them all to PNG before storing them.
106
+ """
107
+
108
+ __tablename__ = "avatar"
109
+
110
+ id: Mapped[int] = mapped_column(primary_key=True)
111
+
112
+ filename: Mapped[str] = mapped_column(unique=True)
113
+ hash: Mapped[str] = mapped_column(unique=True)
114
+ height: Mapped[int] = mapped_column()
115
+ width: Mapped[int] = mapped_column()
116
+
117
+ # this is only used when avatars are available as HTTP URLs and do not
118
+ # have a legacy_id
119
+ url: Mapped[Optional[str]] = mapped_column(default=None)
120
+ etag: Mapped[Optional[str]] = mapped_column(default=None)
121
+ last_modified: Mapped[Optional[str]] = mapped_column(default=None)
122
+
123
+ contacts: Mapped[list["Contact"]] = relationship(back_populates="avatar")
124
+ rooms: Mapped[list["Room"]] = relationship(back_populates="avatar")
125
+
126
+
127
+ class Contact(Base):
128
+ """
129
+ Legacy contacts
130
+ """
131
+
132
+ __tablename__ = "contact"
133
+ __table_args__ = (
134
+ UniqueConstraint("user_account_id", "legacy_id"),
135
+ UniqueConstraint("user_account_id", "jid"),
136
+ )
137
+
138
+ id: Mapped[int] = mapped_column(primary_key=True)
139
+ user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
140
+ user: Mapped[GatewayUser] = relationship(back_populates="contacts")
141
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
142
+
143
+ jid: Mapped[JID] = mapped_column()
144
+
145
+ avatar_id: Mapped[int] = mapped_column(ForeignKey("avatar.id"), nullable=True)
146
+ avatar: Mapped[Avatar] = relationship(back_populates="contacts")
147
+
148
+ nick: Mapped[Optional[str]] = mapped_column(nullable=True)
149
+
150
+ cached_presence: Mapped[bool] = mapped_column(default=False)
151
+ last_seen: Mapped[Optional[datetime]] = mapped_column(nullable=True)
152
+ ptype: Mapped[Optional[str]] = mapped_column(nullable=True)
153
+ pstatus: Mapped[Optional[str]] = mapped_column(nullable=True)
154
+ pshow: Mapped[Optional[str]] = mapped_column(nullable=True)
155
+ caps_ver: Mapped[Optional[str]] = mapped_column(nullable=True)
156
+
157
+ is_friend: Mapped[bool] = mapped_column(default=False)
158
+ added_to_roster: Mapped[bool] = mapped_column(default=False)
159
+ sent_order: Mapped[list["ContactSent"]] = relationship(
160
+ back_populates="contact", cascade="all, delete-orphan"
161
+ )
162
+
163
+ extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(
164
+ default=None, nullable=True
165
+ )
166
+ updated: Mapped[bool] = mapped_column(default=False)
167
+
168
+ vcard: Mapped[Optional[str]] = mapped_column()
169
+ vcard_fetched: Mapped[bool] = mapped_column(default=False)
170
+
171
+ participants: Mapped[list["Participant"]] = relationship(back_populates="contact")
172
+
173
+ avatar_legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
174
+
175
+ client_type: Mapped[ClientType] = mapped_column(nullable=False, default="pc")
176
+
177
+
178
+ class ContactSent(Base):
179
+ """
180
+ Keep track of XMPP msg ids sent by a specific contact for networks in which
181
+ all messages need to be marked as read.
182
+
183
+ (XMPP displayed markers convey a "read up to here" semantic.)
184
+ """
185
+
186
+ __tablename__ = "contact_sent"
187
+ __table_args__ = (UniqueConstraint("contact_id", "msg_id"),)
188
+
189
+ id: Mapped[int] = mapped_column(primary_key=True)
190
+ contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
191
+ contact: Mapped[Contact] = relationship(back_populates="sent_order")
192
+ msg_id: Mapped[str] = mapped_column()
193
+
194
+
195
+ class Room(Base):
196
+ """
197
+ Legacy room
198
+ """
199
+
200
+ __table_args__ = (
201
+ UniqueConstraint(
202
+ "user_account_id", "legacy_id", name="uq_room_user_account_id_legacy_id"
203
+ ),
204
+ UniqueConstraint("user_account_id", "jid", name="uq_room_user_account_id_jid"),
205
+ )
206
+
207
+ __tablename__ = "room"
208
+ id: Mapped[int] = mapped_column(primary_key=True)
209
+ user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
210
+ user: Mapped[GatewayUser] = relationship(back_populates="rooms")
211
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
212
+
213
+ jid: Mapped[JID] = mapped_column(nullable=False)
214
+
215
+ avatar_id: Mapped[int] = mapped_column(ForeignKey("avatar.id"), nullable=True)
216
+ avatar: Mapped[Avatar] = relationship(back_populates="rooms")
217
+
218
+ name: Mapped[Optional[str]] = mapped_column(nullable=True)
219
+ description: Mapped[Optional[str]] = mapped_column(nullable=True)
220
+ subject: Mapped[Optional[str]] = mapped_column(nullable=True)
221
+ subject_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
222
+ subject_setter: Mapped[Optional[str]] = mapped_column(nullable=True)
223
+
224
+ n_participants: Mapped[Optional[int]] = mapped_column(default=None)
225
+
226
+ muc_type: Mapped[Optional[MucType]] = mapped_column(default=MucType.GROUP)
227
+
228
+ user_nick: Mapped[Optional[str]] = mapped_column()
229
+ user_resources: Mapped[Optional[str]] = mapped_column(nullable=True)
230
+
231
+ participants_filled: Mapped[bool] = mapped_column(default=False)
232
+ history_filled: Mapped[bool] = mapped_column(default=False)
233
+
234
+ extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
235
+ updated: Mapped[bool] = mapped_column(default=False)
236
+
237
+ participants: Mapped[list["Participant"]] = relationship(
238
+ back_populates="room",
239
+ primaryjoin="Participant.room_id == Room.id",
240
+ cascade="all, delete-orphan",
241
+ )
242
+
243
+ avatar_legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
244
+
245
+
246
+ class ArchivedMessage(Base):
247
+ """
248
+ Messages of rooms, that we store to act as a MAM server
249
+ """
250
+
251
+ __tablename__ = "mam"
252
+ __table_args__ = (UniqueConstraint("room_id", "stanza_id"),)
253
+
254
+ id: Mapped[int] = mapped_column(primary_key=True)
255
+ room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
256
+
257
+ stanza_id: Mapped[str] = mapped_column(nullable=False)
258
+ timestamp: Mapped[datetime] = mapped_column(nullable=False)
259
+ author_jid: Mapped[JID] = mapped_column(nullable=False)
260
+ source: Mapped[ArchivedMessageSource] = mapped_column(nullable=False)
261
+ legacy_id: Mapped[Optional[str]] = mapped_column(nullable=True)
262
+
263
+ stanza: Mapped[str] = mapped_column(nullable=False)
264
+
265
+
266
+ class XmppToLegacyIds(Base):
267
+ """
268
+ XMPP-client generated IDs, and mapping to the corresponding legacy IDs
269
+ """
270
+
271
+ __tablename__ = "xmpp_to_legacy_ids"
272
+ __table_args__ = (
273
+ Index("xmpp_legacy", "user_account_id", "xmpp_id", "legacy_id", unique=True),
274
+ )
275
+ id: Mapped[int] = mapped_column(primary_key=True)
276
+ user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
277
+ user: Mapped[GatewayUser] = relationship(back_populates="xmpp_to_legacy")
278
+
279
+ xmpp_id: Mapped[str] = mapped_column(nullable=False)
280
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
281
+
282
+ type: Mapped[XmppToLegacyEnum] = mapped_column(nullable=False)
283
+
284
+
285
+ class Attachment(Base):
286
+ """
287
+ Legacy attachments
288
+ """
289
+
290
+ __tablename__ = "attachment"
291
+
292
+ id: Mapped[int] = mapped_column(primary_key=True)
293
+ user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
294
+ user: Mapped[GatewayUser] = relationship(back_populates="attachments")
295
+
296
+ legacy_file_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
297
+ url: Mapped[str] = mapped_column(index=True, nullable=False)
298
+ sims: Mapped[Optional[str]] = mapped_column()
299
+ sfs: Mapped[Optional[str]] = mapped_column()
300
+
301
+
302
+ class LegacyIdsMulti(Base):
303
+ """
304
+ Legacy messages with multiple attachments are split as several XMPP messages,
305
+ this table and the next maps a single legacy ID to multiple XMPP IDs.
306
+ """
307
+
308
+ __tablename__ = "legacy_ids_multi"
309
+ __table_args__ = (
310
+ Index(
311
+ "legacy_ids_multi_user_account_id_legacy_id",
312
+ "user_account_id",
313
+ "legacy_id",
314
+ unique=True,
315
+ ),
316
+ )
317
+ id: Mapped[int] = mapped_column(primary_key=True)
318
+ user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
319
+
320
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
321
+ xmpp_ids: Mapped[list["XmppIdsMulti"]] = relationship(
322
+ back_populates="legacy_ids_multi", cascade="all, delete-orphan"
323
+ )
324
+
325
+
326
+ class XmppIdsMulti(Base):
327
+ __tablename__ = "xmpp_ids_multi"
328
+ __table_args__ = (
329
+ Index(
330
+ "legacy_ids_multi_user_account_id_xmpp_id",
331
+ "user_account_id",
332
+ "xmpp_id",
333
+ unique=True,
334
+ ),
335
+ )
336
+ id: Mapped[int] = mapped_column(primary_key=True)
337
+ user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
338
+
339
+ xmpp_id: Mapped[str] = mapped_column(nullable=False)
340
+
341
+ legacy_ids_multi_id: Mapped[int] = mapped_column(ForeignKey("legacy_ids_multi.id"))
342
+ legacy_ids_multi: Mapped[LegacyIdsMulti] = relationship(back_populates="xmpp_ids")
343
+
344
+
345
+ participant_hats = sa.Table(
346
+ "participant_hats",
347
+ Base.metadata,
348
+ sa.Column("participant_id", ForeignKey("participant.id"), primary_key=True),
349
+ sa.Column("hat_id", ForeignKey("hat.id"), primary_key=True),
350
+ )
351
+
352
+
353
+ class Hat(Base):
354
+ __tablename__ = "hat"
355
+ __table_args__ = (UniqueConstraint("title", "uri"),)
356
+
357
+ id: Mapped[int] = mapped_column(primary_key=True)
358
+ title: Mapped[str] = mapped_column()
359
+ uri: Mapped[str] = mapped_column()
360
+ participants: Mapped[list["Participant"]] = relationship(
361
+ secondary=participant_hats, back_populates="hats"
362
+ )
363
+
364
+
365
+ class Participant(Base):
366
+ __tablename__ = "participant"
367
+
368
+ id: Mapped[int] = mapped_column(primary_key=True)
369
+
370
+ room_id: Mapped[int] = mapped_column(ForeignKey("room.id"), nullable=False)
371
+ room: Mapped[Room] = relationship(
372
+ back_populates="participants", primaryjoin=Room.id == room_id
373
+ )
374
+
375
+ contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=True)
376
+ contact: Mapped[Contact] = relationship(lazy=False, back_populates="participants")
377
+
378
+ is_user: Mapped[bool] = mapped_column(default=False)
379
+
380
+ affiliation: Mapped[MucAffiliation] = mapped_column(default="member")
381
+ role: Mapped[MucRole] = mapped_column(default="participant")
382
+
383
+ presence_sent: Mapped[bool] = mapped_column(default=False)
384
+
385
+ resource: Mapped[Optional[str]] = mapped_column(default=None)
386
+ nickname: Mapped[str] = mapped_column(nullable=True, default=None)
387
+
388
+ hats: Mapped[list["Hat"]] = relationship(
389
+ secondary=participant_hats, back_populates="participants"
390
+ )
391
+
392
+ extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
393
+
394
+
395
+ class Bob(Base):
396
+ __tablename__ = "bob"
397
+
398
+ id: Mapped[int] = mapped_column(primary_key=True)
399
+ file_name: Mapped[str] = mapped_column(nullable=False)
400
+
401
+ sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
402
+ sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
403
+ sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
404
+
405
+ content_type: Mapped[Optional[str]] = mapped_column(nullable=False)