artemis-model 0.1.66__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.
@@ -0,0 +1,17 @@
1
+ from artemis_model.album import * # noqa
2
+ from artemis_model.artist import * # noqa
3
+ from artemis_model.auth import * # noqa
4
+ from artemis_model.category import * # noqa
5
+ from artemis_model.dj_set import * # noqa
6
+ from artemis_model.genre import * # noqa
7
+ from artemis_model.location import * # noqa
8
+ from artemis_model.message import * # noqa
9
+ from artemis_model.organization import * # noqa
10
+ from artemis_model.playlist import * # noqa
11
+ from artemis_model.schedule import * # noqa
12
+ from artemis_model.setting import * # noqa
13
+ from artemis_model.track import * # noqa
14
+ from artemis_model.user import * # noqa
15
+ from artemis_model.zone import * # noqa
16
+ from artemis_model.otp import * # noqa
17
+
artemis_model/album.py ADDED
@@ -0,0 +1,73 @@
1
+ """Album models"""
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import Computed, ForeignKey, func, literal, text
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from artemis_model.base import CustomSyncBase, CustomBase, TSVector, TimeStampMixin
9
+
10
+ from sqlalchemy.ext.declarative import declared_attr
11
+
12
+
13
+ def to_tsvector_ix(*columns):
14
+ s = " || ' ' || ".join(columns)
15
+ return func.to_tsvector(literal("english"), text(s))
16
+
17
+
18
+ class AlbumMixin(TimeStampMixin):
19
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
20
+ name: Mapped[str] = mapped_column(nullable=False)
21
+ description: Mapped[str | None] = mapped_column(nullable=True)
22
+ entry_date: Mapped[datetime] = mapped_column(
23
+ nullable=False, default=datetime.utcnow
24
+ )
25
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
26
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
27
+ is_internal: Mapped[bool] = mapped_column(nullable=False, default=False)
28
+
29
+ name_tsv = mapped_column(
30
+ TSVector(),
31
+ Computed("to_tsvector('english', name)", persisted=True),
32
+ )
33
+
34
+ # __table_args__ = (
35
+ # Index("fts_ix_album_name_tsv", to_tsvector_ix("name"), postgresql_using="gin"),
36
+ # )
37
+
38
+ @declared_attr
39
+ def artists(cls) -> Mapped[list["Artist"]]:
40
+ return relationship(secondary="album_artist_assoc", back_populates="albums")
41
+
42
+ @declared_attr
43
+ def tracks(cls) -> Mapped[list["Track"]]:
44
+ return relationship(
45
+ "Track",
46
+ back_populates="album",
47
+ cascade="all, delete-orphan",
48
+ )
49
+
50
+
51
+ class AlbumSync(CustomSyncBase, AlbumMixin):
52
+ pass
53
+
54
+
55
+ class Album(CustomBase, AlbumMixin):
56
+ pass
57
+
58
+
59
+ class AlbumArtistAssocMixin():
60
+ album_id: Mapped[int] = mapped_column(
61
+ ForeignKey("album.id"), primary_key=True, nullable=False
62
+ )
63
+ artist_id: Mapped[int] = mapped_column(
64
+ ForeignKey("artist.id"), primary_key=True, nullable=False
65
+ )
66
+
67
+
68
+ class AlbumArtistAssocSync(CustomSyncBase, AlbumArtistAssocMixin):
69
+ pass
70
+
71
+
72
+ class AlbumArtistAssoc(CustomBase, AlbumArtistAssocMixin):
73
+ pass
@@ -0,0 +1,51 @@
1
+ """Artist models"""
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import Computed, func, literal, text
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from artemis_model.base import CustomBase, CustomSyncBase, TSVector, TimeStampMixin
9
+
10
+ from sqlalchemy.ext.declarative import declared_attr
11
+
12
+
13
+ def to_tsvector_ix(*columns):
14
+ s = " || ' ' || ".join(columns)
15
+ return func.to_tsvector(literal("english"), text(s))
16
+
17
+
18
+ class ArtistMixin(TimeStampMixin):
19
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
20
+ name: Mapped[str] = mapped_column(nullable=False)
21
+ entry_date: Mapped[datetime] = mapped_column(
22
+ nullable=False, default=datetime.utcnow
23
+ )
24
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
25
+ is_internal: Mapped[bool] = mapped_column(nullable=False, default=False)
26
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
27
+
28
+ name_tsv = mapped_column(
29
+ TSVector(),
30
+ Computed("to_tsvector('english', name)", persisted=True),
31
+ )
32
+
33
+ # __table_args__ = (
34
+ # Index("fts_ix_artist_name_tsv", to_tsvector_ix("name"), postgresql_using="gin"),
35
+ # )
36
+
37
+ @declared_attr
38
+ def albums(cls) -> Mapped[list["Album"]]:
39
+ return relationship(secondary="album_artist_assoc", back_populates="artists")
40
+
41
+ @declared_attr
42
+ def tracks(cls) -> Mapped[list["Track"]]:
43
+ return relationship("Track", back_populates="artist", cascade="all, delete-orphan")
44
+
45
+
46
+ class ArtistSync(CustomSyncBase, ArtistMixin):
47
+ pass
48
+
49
+
50
+ class Artist(CustomBase, ArtistMixin):
51
+ pass
artemis_model/auth.py ADDED
@@ -0,0 +1,119 @@
1
+ """Auth models."""
2
+ import uuid
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from sqlalchemy import UUID, DateTime, ForeignKey, LargeBinary
7
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
8
+ from sqlalchemy.ext.declarative import declared_attr
9
+
10
+ from artemis_model.base import CustomSyncBase, TimeStampMixin, CustomBase
11
+
12
+
13
+ class UserUnverifiedAccountMixin(TimeStampMixin):
14
+ """
15
+ This table is used to store the account info of users who have requested an account but have not yet
16
+ been verified. Once the user has been verified, the account will be moved to the UserAccount table.
17
+ """
18
+
19
+ id: Mapped[uuid.UUID] = mapped_column(
20
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
21
+ )
22
+ email: Mapped[str] = mapped_column(index=True)
23
+ name: Mapped[str] = mapped_column(nullable=False)
24
+ mobile: Mapped[str] = mapped_column(nullable=True, index=True)
25
+ is_root_user: Mapped[bool] = mapped_column(default=True)
26
+ is_email_verified: Mapped[bool] = mapped_column(default=False)
27
+ is_mobile_verified: Mapped[bool] = mapped_column(default=False)
28
+ is_onboarded: Mapped[bool] = mapped_column(default=False)
29
+ password = mapped_column(LargeBinary, nullable=False)
30
+ provider: Mapped[str] = mapped_column(default="internal")
31
+
32
+
33
+ class UserUnverifiedAccountSync(CustomSyncBase, UserUnverifiedAccountMixin):
34
+ pass
35
+
36
+
37
+ class UserUnverifiedAccount(CustomBase, UserUnverifiedAccountMixin):
38
+ pass
39
+
40
+
41
+ class UserAccountMixin(TimeStampMixin):
42
+ """
43
+ This table is used to store the account info of users who have been verified.
44
+ """
45
+
46
+ id: Mapped[uuid.UUID] = mapped_column(
47
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
48
+ )
49
+ email: Mapped[str] = mapped_column(index=True, unique=True)
50
+ name: Mapped[str] = mapped_column(nullable=False)
51
+ mobile: Mapped[Optional[str]] = mapped_column(
52
+ nullable=True, unique=True, index=True
53
+ )
54
+ password = mapped_column(LargeBinary, nullable=True)
55
+ provider: Mapped[str] = mapped_column(nullable=False)
56
+ oauth_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
57
+ image_url: Mapped[Optional[str]] = mapped_column(nullable=True)
58
+ is_email_verified: Mapped[bool] = mapped_column(default=False)
59
+ is_mobile_verified: Mapped[bool] = mapped_column(default=False)
60
+ is_root_user: Mapped[bool] = mapped_column(default=True)
61
+ is_super_admin: Mapped[bool] = mapped_column(default=False)
62
+ disabled: Mapped[bool] = mapped_column(default=False)
63
+ disabled_reason: Mapped[Optional[str]] = mapped_column(nullable=True)
64
+ is_onboarded: Mapped[bool] = mapped_column(default=False)
65
+
66
+ @declared_attr
67
+ def login_histories(cls) -> Mapped["LoginHistory"]:
68
+ return relationship("LoginHistory", back_populates="account")
69
+
70
+ @declared_attr
71
+ def user(cls) -> Mapped["User"]:
72
+ return relationship("User", back_populates="account")
73
+
74
+
75
+ class UserAccountSync(CustomSyncBase, UserAccountMixin):
76
+ pass
77
+
78
+
79
+ class UserAccount(CustomBase, UserAccountMixin):
80
+ pass
81
+
82
+
83
+ class LoginHistoryMixin:
84
+ """
85
+ This table is used to store the login history of users.
86
+ """
87
+ id: Mapped[int] = mapped_column(
88
+ primary_key=True, autoincrement=True, nullable=False
89
+ )
90
+ account_id: Mapped[uuid.UUID] = mapped_column(
91
+ ForeignKey("user_account.id"), nullable=False, index=True
92
+ )
93
+ ip_address: Mapped[str] = mapped_column(nullable=True)
94
+ created_at = mapped_column(DateTime, default=datetime.utcnow)
95
+
96
+ @declared_attr
97
+ def account(cls) -> Mapped["UserAccount"]:
98
+ return relationship("UserAccount", back_populates="login_histories")
99
+
100
+
101
+ class LoginHistorySync(CustomSyncBase, LoginHistoryMixin):
102
+ pass
103
+
104
+
105
+ class LoginHistory(CustomBase, LoginHistoryMixin):
106
+ pass
107
+
108
+
109
+ class OAuthCsrfStateMixin(TimeStampMixin):
110
+ id: Mapped[str] = mapped_column(primary_key=True, unique=True)
111
+ client_base_url: Mapped[str] = mapped_column(nullable=True)
112
+
113
+
114
+ class OAuthCsrfStateSync(CustomSyncBase, OAuthCsrfStateMixin):
115
+ pass
116
+
117
+
118
+ class OAuthCsrfState(CustomBase, OAuthCsrfStateMixin):
119
+ pass
artemis_model/base.py ADDED
@@ -0,0 +1,222 @@
1
+ import re
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ from sqlalchemy.orm import (DeclarativeBase, Mapped, declared_attr,
6
+ mapped_column, object_session, relationship)
7
+ from sqlalchemy.ext.asyncio import AsyncAttrs
8
+ from sqlalchemy import Column, Uuid, event, inspect, TypeDecorator
9
+ from sqlalchemy.dialects.postgresql import TSVECTOR
10
+
11
+
12
+ def resolve_table_name(name: str) -> str:
13
+ """Resolves table names to their mapped names."""
14
+ names = re.split("(?=[A-Z])", name) # noqa
15
+ print(names)
16
+ return "_".join([x.lower() for x in names if x])
17
+
18
+
19
+ class CustomBase(DeclarativeBase, AsyncAttrs):
20
+ __repr_attrs__: list[Any] = []
21
+ __repr_max_length__ = 15
22
+
23
+ @declared_attr
24
+ def __tablename__(self) -> str:
25
+ return resolve_table_name(self.__name__)
26
+
27
+ def dict(self) -> dict:
28
+ """Returns a dict representation of a model."""
29
+ return {c.name: getattr(self, c.name) for c in self.__table__.columns}
30
+
31
+ @property
32
+ def _id_str(self) -> str:
33
+ ids = inspect(self).identity
34
+ if ids:
35
+ return "-".join([str(x) for x in ids]) if len(ids) > 1 else str(ids[0])
36
+ else:
37
+ return "None"
38
+
39
+ @property
40
+ def _repr_attrs_str(self) -> str:
41
+ max_length = self.__repr_max_length__
42
+
43
+ values = []
44
+ single = len(self.__repr_attrs__) == 1
45
+ for key in self.__repr_attrs__:
46
+ if not hasattr(self, key):
47
+ raise KeyError(
48
+ "{} has incorrect attribute '{}' in "
49
+ "__repr__attrs__".format(self.__class__, key)
50
+ )
51
+ value = getattr(self, key)
52
+ wrap_in_quote = isinstance(value, str)
53
+
54
+ value = str(value)
55
+ if len(value) > max_length:
56
+ value = value[:max_length] + "..."
57
+
58
+ if wrap_in_quote:
59
+ value = "'{}'".format(value)
60
+ values.append(value if single else "{}:{}".format(key, value))
61
+
62
+ return " ".join(values)
63
+
64
+ def __repr__(self) -> str:
65
+ # get id like '#123'
66
+ id_str = ("#" + self._id_str) if self._id_str else ""
67
+ # join class name, id and repr_attrs
68
+ return "<{} {}{}>".format(
69
+ self.__class__.__name__,
70
+ id_str,
71
+ " " + self._repr_attrs_str if self._repr_attrs_str else "",
72
+ )
73
+
74
+
75
+ class AuditMixin(object):
76
+ created_by = Column(Uuid, nullable=True)
77
+ updated_by = Column(Uuid, nullable=True)
78
+
79
+ @declared_attr
80
+ def created_by_user(cls):
81
+ return relationship(
82
+ "User",
83
+ foreign_keys=[cls.created_by],
84
+ primaryjoin="User.id==%s.created_by" % cls.__name__,
85
+ )
86
+
87
+ @declared_attr
88
+ def updated_by_user(cls):
89
+ return relationship(
90
+ "User",
91
+ foreign_keys=[cls.updated_by],
92
+ primaryjoin="User.id==%s.updated_by" % cls.__name__,
93
+ )
94
+
95
+ @staticmethod
96
+ def _updated_info(mapper: Any, connection: Any, target: Any) -> None:
97
+ s = object_session(target)
98
+ target.updated_at = datetime.utcnow()
99
+ if user_id := getattr(s, "user_id", None):
100
+ target.updated_by = user_id
101
+
102
+ @staticmethod
103
+ def _created_info(mapper: Any, connection: Any, target: Any) -> None:
104
+ s = object_session(target)
105
+ if user_id := getattr(s, "user_id", None):
106
+ target.created_by = user_id
107
+
108
+ @classmethod
109
+ def get_session_user_id(cls, connection):
110
+ return connection.info.get("user_id")
111
+
112
+ @classmethod
113
+ def __declare_last__(cls) -> None:
114
+ event.listen(cls, "before_insert", cls._created_info)
115
+ event.listen(cls, "before_update", cls._updated_info)
116
+
117
+
118
+ class TimeStampMixin(object):
119
+ """Timestamping mixin"""
120
+
121
+ created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
122
+ created_at._creation_order = 9998
123
+ updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
124
+ updated_at._creation_order = 9998
125
+
126
+ @staticmethod
127
+ def _updated_at(mapper, connection, target):
128
+ target.updated_at = datetime.utcnow()
129
+
130
+ @classmethod
131
+ def __declare_last__(cls):
132
+ event.listen(cls, "before_update", cls._updated_at)
133
+
134
+
135
+ # class InheritanceMixin(object):
136
+ # @declared_attr
137
+ # def __mapper_args__(cls):
138
+ # return {
139
+ # 'polymorphic_identity': cls.__name__.lower(),
140
+ # }
141
+
142
+
143
+ # class CustomBase(Base, AsyncAttrs):
144
+ # pass
145
+
146
+
147
+ # class SyncInheritanceMixin(object):
148
+ # type = Column(String(50))
149
+
150
+ # @declared_attr
151
+ # def __mapper_args__(cls):
152
+ # return {
153
+ # 'polymorphic_identity': cls.__name__.lower(),
154
+ # 'polymorphic_on': 'type',
155
+ # }
156
+
157
+
158
+ class CustomSyncBase(DeclarativeBase):
159
+ __repr_attrs__: list[Any] = []
160
+ __repr_max_length__ = 15
161
+ __abstract__ = True
162
+
163
+ @declared_attr
164
+ def __tablename__(self) -> str:
165
+ return resolve_table_name(self.__name__)
166
+
167
+ """ Is this missed here or not moved intentionally?"""
168
+
169
+ # def serializable_dict(self) -> Dict[str, Any]:
170
+ # d = {col.name: getattr(self, col.name) for col in self.__table__.columns}
171
+ # return orjson.loads(orjson.dumps(jsonable_encoder(d)))
172
+
173
+ def dict(self) -> dict:
174
+ """Returns a dict representation of a model."""
175
+ return {c.name: getattr(self, c.name) for c in self.__table__.columns}
176
+
177
+ @property
178
+ def _id_str(self) -> str:
179
+ ids = inspect(self).identity
180
+ if ids:
181
+ return "-".join([str(x) for x in ids]) if len(ids) > 1 else str(ids[0])
182
+ else:
183
+ return "None"
184
+
185
+ @property
186
+ def _repr_attrs_str(self) -> str:
187
+ max_length = self.__repr_max_length__
188
+
189
+ values = []
190
+ single = len(self.__repr_attrs__) == 1
191
+ for key in self.__repr_attrs__:
192
+ if not hasattr(self, key):
193
+ raise KeyError(
194
+ "{} has incorrect attribute '{}' in "
195
+ "__repr__attrs__".format(self.__class__, key)
196
+ )
197
+ value = getattr(self, key)
198
+ wrap_in_quote = isinstance(value, str)
199
+
200
+ value = str(value)
201
+ if len(value) > max_length:
202
+ value = value[:max_length] + "..."
203
+
204
+ if wrap_in_quote:
205
+ value = "'{}'".format(value)
206
+ values.append(value if single else "{}:{}".format(key, value))
207
+
208
+ return " ".join(values)
209
+
210
+ def __repr__(self) -> str:
211
+ # get id like '#123'
212
+ id_str = ("#" + self._id_str) if self._id_str else ""
213
+ # join class name, id and repr_attrs
214
+ return "<{} {}{}>".format(
215
+ self.__class__.__name__,
216
+ id_str,
217
+ " " + self._repr_attrs_str if self._repr_attrs_str else "",
218
+ )
219
+
220
+
221
+ class TSVector(TypeDecorator):
222
+ impl = TSVECTOR
@@ -0,0 +1,60 @@
1
+ from sqlalchemy import ARRAY, Computed, String, func, literal, text
2
+ from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
3
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
4
+
5
+ from artemis_model.base import CustomSyncBase, TimeStampMixin, TSVector, CustomBase
6
+
7
+ from sqlalchemy.ext.declarative import declared_attr
8
+
9
+ from artemis_model.playlist import PlaylistCategoryAssoc
10
+
11
+
12
+ def to_tsvector_ix(*columns):
13
+ s = " || ' ' || ".join(columns)
14
+ return func.to_tsvector(literal("english"), text(s))
15
+
16
+
17
+ class CategoryMixin(TimeStampMixin):
18
+ id: Mapped[int] = mapped_column(primary_key=True)
19
+ name: Mapped[str] = mapped_column(nullable=False)
20
+ sub_categories: Mapped[list[str]] = mapped_column(ARRAY(String), default=[])
21
+
22
+ name_tsv = mapped_column(
23
+ TSVector(),
24
+ Computed("to_tsvector('english', name)", persisted=True),
25
+ )
26
+
27
+ @declared_attr
28
+ def category_playlist_associations(cls) -> Mapped[list["PlaylistCategoryAssoc"]]:
29
+ return relationship(back_populates="category", cascade="all, delete-orphan")
30
+
31
+ @declared_attr
32
+ def playlists(cls) -> AssociationProxy[list["Playlist"]]:
33
+ return association_proxy(
34
+ "playlist_category_assoc",
35
+ "playlist",
36
+ creator=lambda p: PlaylistCategoryAssoc(playlist_id=p),
37
+ )
38
+
39
+ # __table_args__ = (
40
+ # Index(
41
+ # "fts_ix_category_name_tsv",
42
+ # to_tsvector_ix("name"),
43
+ # postgresql_using="gin",
44
+ # ),
45
+ # )
46
+
47
+ @property
48
+ def is_subcategory(self) -> bool:
49
+ if self.sub_categories:
50
+ return True
51
+
52
+ return False
53
+
54
+
55
+ class CategorySync(CustomSyncBase, CategoryMixin):
56
+ pass
57
+
58
+
59
+ class Category(CustomBase, CategoryMixin):
60
+ pass
@@ -0,0 +1,120 @@
1
+ import uuid
2
+ from datetime import datetime
3
+
4
+ from sqlalchemy import Computed, ForeignKey, ARRAY, Integer
5
+ from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from artemis_model.base import CustomBase, CustomSyncBase, TSVector, TimeStampMixin, AuditMixin
9
+ from sqlalchemy.ext.declarative import declared_attr
10
+
11
+
12
+ class DjSetGenreAssocMixin(TimeStampMixin):
13
+ dj_set_id: Mapped[uuid.UUID] = mapped_column(
14
+ ForeignKey("dj_set.id"), primary_key=True, nullable=False
15
+ )
16
+ genre_id: Mapped[uuid.UUID] = mapped_column(
17
+ ForeignKey("genre.id"), primary_key=True, nullable=False
18
+ )
19
+ weight: Mapped[int] = mapped_column(nullable=False, default=0)
20
+
21
+ @declared_attr
22
+ def genre(cls) -> Mapped["Genre"]:
23
+ return relationship(back_populates="dj_set_genre_associations")
24
+
25
+
26
+ class DjSetGenreAssocSync(CustomSyncBase, DjSetGenreAssocMixin):
27
+ pass
28
+
29
+
30
+ class DjSetGenreAssoc(CustomBase, DjSetGenreAssocMixin):
31
+ pass
32
+
33
+
34
+ class DjSetMixin(TimeStampMixin, AuditMixin):
35
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
36
+ name: Mapped[str] = mapped_column(nullable=False, unique=True)
37
+ organization_id: Mapped[uuid.UUID] = mapped_column(
38
+ ForeignKey("organization.id"), nullable=True, index=True
39
+ )
40
+ entry_date: Mapped[datetime] = mapped_column(
41
+ nullable=False, default=datetime.utcnow
42
+ )
43
+ cover_image: Mapped[str] = mapped_column(nullable=True)
44
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
45
+ legacy_id: Mapped[str] = mapped_column(nullable=True)
46
+ description: Mapped[str | None] = mapped_column(nullable=True)
47
+ mood: Mapped[list[int]] = mapped_column(ARRAY(Integer), nullable=False, default=[0, 0])
48
+ decades: Mapped[list[int] | None] = mapped_column(ARRAY(Integer), nullable=True)
49
+ total_duration: Mapped[int] = mapped_column(nullable=False, default=0)
50
+
51
+ name_tsv = mapped_column(
52
+ TSVector(),
53
+ Computed("to_tsvector('english', name)", persisted=True),
54
+ )
55
+
56
+ @declared_attr
57
+ def organization(cls) -> Mapped["Organization"]:
58
+ return relationship(back_populates="dj_sets")
59
+
60
+ @declared_attr
61
+ def genre_ids(cls) -> AssociationProxy[list["Genre"] | None]:
62
+ return association_proxy(
63
+ "dj_set_genre_associations",
64
+ "genre",
65
+ creator=lambda genre: DjSetGenreAssoc(genre_id=genre.get("id"), weight=genre.get("weight", 0)),
66
+ )
67
+
68
+ @declared_attr
69
+ def dj_set_genre_associations(cls) -> Mapped[list["DjSetGenreAssoc"]]:
70
+ return relationship(cascade="all, delete-orphan")
71
+
72
+ @declared_attr
73
+ def dj_set_track_associations(cls) -> Mapped[list["DjSetTrackAssoc"]]:
74
+ return relationship(
75
+ cascade="all, delete-orphan"
76
+ )
77
+
78
+ @declared_attr
79
+ def genres(cls) -> Mapped[list["Genre"]]:
80
+ return relationship(
81
+ secondary="dj_set_genre_assoc",
82
+ lazy="joined",
83
+ viewonly=True
84
+ )
85
+
86
+
87
+ @declared_attr
88
+ def tracks(cls) -> Mapped[list["Track"]]:
89
+ return relationship(
90
+ secondary="dj_set_track_assoc", viewonly=True
91
+ )
92
+
93
+
94
+ class DjSetSync(CustomSyncBase, DjSetMixin):
95
+ pass
96
+
97
+
98
+ class DjSet(CustomBase, DjSetMixin):
99
+ pass
100
+
101
+
102
+ class DjSetTrackAssocMixin(TimeStampMixin):
103
+ dj_set_id: Mapped[uuid.UUID] = mapped_column(
104
+ ForeignKey("dj_set.id"), primary_key=True, nullable=False
105
+ )
106
+ track_id: Mapped[uuid.UUID] = mapped_column(
107
+ ForeignKey("track.id"), primary_key=True, nullable=False
108
+ )
109
+
110
+ @declared_attr
111
+ def track(cls) -> Mapped["Track"]:
112
+ return relationship(back_populates="dj_set_track_associations")
113
+
114
+
115
+ class DjSetTrackAssocSync(CustomSyncBase, DjSetTrackAssocMixin):
116
+ pass
117
+
118
+
119
+ class DjSetTrackAssoc(CustomBase, DjSetTrackAssocMixin):
120
+ pass