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.
artemis_model/genre.py ADDED
@@ -0,0 +1,52 @@
1
+
2
+ from sqlalchemy import Computed, func, literal, text
3
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
4
+
5
+ from artemis_model.base import CustomBase, CustomSyncBase, TSVector, TimeStampMixin
6
+
7
+ from sqlalchemy.ext.declarative import declared_attr
8
+
9
+
10
+ def to_tsvector_ix(*columns):
11
+ s = " || ' ' || ".join(columns)
12
+ return func.to_tsvector(literal("english"), text(s))
13
+
14
+
15
+ class GenreMixin(TimeStampMixin):
16
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
17
+ name: Mapped[str] = mapped_column(nullable=False)
18
+ parent_id: Mapped[int | None] = mapped_column(nullable=True)
19
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
20
+ legacy_id: Mapped[str] = mapped_column(nullable=False)
21
+
22
+ name_tsv = mapped_column(
23
+ TSVector(),
24
+ Computed("to_tsvector('english', name)", persisted=True),
25
+ )
26
+
27
+ @declared_attr
28
+ def tracks(cls) -> Mapped[list["Track"]]:
29
+ return relationship(
30
+ "Track", secondary="track_genre_assoc", back_populates="genres"
31
+ )
32
+
33
+ @declared_attr
34
+ def dj_set_genre_associations(cls) -> Mapped[list["DjSetGenreAssoc"]]:
35
+ return relationship(
36
+ back_populates="genre", cascade="all, delete-orphan"
37
+ )
38
+
39
+ def dj_sets(cls) -> Mapped[list["DjSet"]]:
40
+ return relationship("DjSet", secondary="dj_set_genre_assoc", back_populates="genre")
41
+
42
+ # __table_args__ = (
43
+ # Index("fts_ix_genre_name_tsv", to_tsvector_ix("name"), postgresql_using="gin"),
44
+ # )
45
+
46
+
47
+ class GenreSync(CustomSyncBase, GenreMixin):
48
+ pass
49
+
50
+
51
+ class Genre(CustomBase, GenreMixin):
52
+ pass
@@ -0,0 +1,154 @@
1
+ import uuid
2
+ from typing import Any, List
3
+
4
+ from slugify import slugify
5
+ from sqlalchemy import UUID, ForeignKey
6
+ from sqlalchemy.event import listen
7
+ from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
8
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
9
+
10
+ from artemis_model.base import TimeStampMixin, CustomSyncBase, CustomBase
11
+
12
+ from sqlalchemy.ext.declarative import declared_attr
13
+
14
+
15
+ class AddressMixin:
16
+ id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
17
+ street: Mapped[str] = mapped_column(nullable=False)
18
+ street2: Mapped[str] = mapped_column(nullable=True)
19
+ city: Mapped[str] = mapped_column(nullable=False)
20
+ state: Mapped[str] = mapped_column(nullable=False)
21
+ zip_code: Mapped[str] = mapped_column(nullable=False)
22
+ country: Mapped[str] = mapped_column(nullable=False)
23
+
24
+ @declared_attr
25
+ def location(cls) -> Mapped["Location"]:
26
+ return relationship(
27
+ uselist=False,
28
+ back_populates="address",
29
+ )
30
+
31
+
32
+ class AddressSync(CustomSyncBase, AddressMixin):
33
+ pass
34
+
35
+
36
+ class Address(CustomBase, AddressMixin):
37
+ pass
38
+
39
+
40
+ class LocationMixin(TimeStampMixin):
41
+ id: Mapped[uuid.UUID] = mapped_column(
42
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
43
+ )
44
+ name: Mapped[str] = mapped_column(nullable=False)
45
+ slug: Mapped[str] = mapped_column(nullable=False)
46
+ organization_id: Mapped[uuid.UUID] = mapped_column(
47
+ ForeignKey("organization.id"), nullable=False, index=True
48
+ )
49
+ location_type: Mapped[str] = mapped_column(nullable=True)
50
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
51
+ group_id: Mapped[int | None] = mapped_column(
52
+ ForeignKey("location_group.id"), nullable=True, index=True
53
+ )
54
+
55
+ address_id = mapped_column(ForeignKey("address.id"), nullable=True)
56
+ legacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=True)
57
+
58
+ @declared_attr
59
+ def organization(cls) -> Mapped["Organization"]:
60
+ return relationship(back_populates="locations")
61
+
62
+ @declared_attr
63
+ def group(cls) -> Mapped["LocationGroup"]:
64
+ return relationship(back_populates="locations")
65
+
66
+ @declared_attr
67
+ def zones(cls) -> Mapped[list["Zone"]]:
68
+ return relationship(back_populates="location")
69
+
70
+ @declared_attr
71
+ def users(cls) -> AssociationProxy[list["User"]]:
72
+ return association_proxy(
73
+ "user_location_associations",
74
+ "user",
75
+ creator=lambda user_id: UserLocationAssoc(user_id=user_id),
76
+ )
77
+
78
+ @declared_attr
79
+ def address(cls) -> Mapped["Address"]:
80
+ return relationship(
81
+ uselist=False,
82
+ back_populates="location",
83
+ )
84
+
85
+ @declared_attr
86
+ def user_location_associations(cls) -> Mapped[list["UserLocationAssoc"]]:
87
+ return relationship(
88
+ cascade="all, delete-orphan",
89
+ )
90
+
91
+ timezone: Mapped[str] = mapped_column(nullable=False, default="America/New_York")
92
+
93
+
94
+ def generate_slug(target: Any, value: Any, old_value: Any, initiator: Any) -> None:
95
+ """Creates a reasonable slug based on location name."""
96
+ if value and (not target.slug or value != old_value):
97
+ target.slug = slugify(value, separator="_")
98
+
99
+
100
+ class LocationSync(CustomSyncBase, LocationMixin):
101
+ pass
102
+
103
+
104
+ listen(LocationSync.name, "set", generate_slug)
105
+
106
+
107
+ class Location(CustomBase, LocationMixin):
108
+ pass
109
+
110
+
111
+ listen(Location.name, "set", generate_slug)
112
+
113
+
114
+ class LocationGroupMixin(TimeStampMixin):
115
+ id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
116
+ organization_id = mapped_column(
117
+ ForeignKey("organization.id"), nullable=False, index=True
118
+ )
119
+ name: Mapped[str] = mapped_column(nullable=False)
120
+ description: Mapped[str] = mapped_column(nullable=True)
121
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
122
+
123
+ @declared_attr
124
+ def locations(cls) -> Mapped[List["Location"]]:
125
+ return relationship(
126
+ back_populates="group",
127
+ )
128
+
129
+
130
+ class LocationGroupSync(CustomSyncBase, LocationGroupMixin):
131
+ pass
132
+
133
+
134
+ class LocationGroup(CustomBase, LocationGroupMixin):
135
+ pass
136
+
137
+
138
+ class UserLocationAssocMixin(TimeStampMixin):
139
+ location_id: Mapped[uuid.UUID] = mapped_column(
140
+ ForeignKey("location.id"), primary_key=True
141
+ )
142
+ user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user.id"), primary_key=True)
143
+
144
+ @declared_attr
145
+ def user(cls) -> Mapped["User"]:
146
+ return relationship(back_populates="user_location_associations")
147
+
148
+
149
+ class UserLocationAssocSync(CustomSyncBase, UserLocationAssocMixin):
150
+ pass
151
+
152
+
153
+ class UserLocationAssoc(CustomBase, UserLocationAssocMixin):
154
+ pass
@@ -0,0 +1,97 @@
1
+ import uuid
2
+ from typing import List
3
+ from sqlalchemy import JSON, ForeignKey
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from artemis_model.base import CustomSyncBase, TimeStampMixin, AuditMixin, CustomBase
7
+
8
+ from sqlalchemy.ext.declarative import declared_attr
9
+
10
+
11
+ class MessageMixin(TimeStampMixin, AuditMixin):
12
+ """
13
+ This table is used to store the message.
14
+ """
15
+ id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True, index=True)
16
+ name: Mapped[str] = mapped_column(nullable=False)
17
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
18
+ s3_link: Mapped[str] = mapped_column(nullable=False)
19
+
20
+ organization_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("organization.id"), nullable=False, index=True)
21
+ message_group_id: Mapped[int] = mapped_column(ForeignKey("message_group.id"), nullable=True, index=True, default=None)
22
+
23
+ @declared_attr
24
+ def organization(cls) -> Mapped["Organization"]:
25
+ return relationship("Organization", back_populates="messages")
26
+
27
+ @declared_attr
28
+ def message_group(cls) -> Mapped["MessageGroup"]:
29
+ return relationship("MessageGroup", back_populates="messages")
30
+
31
+ @declared_attr
32
+ def play_details(cls) -> Mapped[List["MessagePlayDetail"]]:
33
+ return relationship("MessagePlayDetail", back_populates="message")
34
+
35
+
36
+ class MessageSync(CustomSyncBase, MessageMixin):
37
+ pass
38
+
39
+
40
+ class Message(CustomBase, MessageMixin):
41
+ pass
42
+
43
+
44
+ class MessagePlayDetailMixin(TimeStampMixin, AuditMixin):
45
+ """
46
+ This table is used to store the message play details.
47
+ Frequency is used only for rotated messages.
48
+ Timesheet is used only for scheduled messages.
49
+ """
50
+ id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True, index=True)
51
+ message_id: Mapped[int] = mapped_column(ForeignKey("message.id"), nullable=False, index=True)
52
+ zone_id: Mapped[int] = mapped_column(ForeignKey("zone.id"), index=True)
53
+
54
+ type: Mapped[str] = mapped_column(nullable=False)
55
+
56
+ timesheet: Mapped[JSON] = mapped_column(JSON, nullable=True)
57
+
58
+ @declared_attr
59
+ def message(cls) -> Mapped["Message"]:
60
+ return relationship("Message", back_populates="play_details")
61
+
62
+ @declared_attr
63
+ def zone(cls) -> Mapped["Zone"]:
64
+ return relationship("Zone", back_populates="message_play_details")
65
+
66
+
67
+ class MessagePlayDetailSync(CustomSyncBase, MessagePlayDetailMixin):
68
+ pass
69
+
70
+
71
+ class MessagePlayDetail(CustomBase, MessagePlayDetailMixin):
72
+ pass
73
+
74
+
75
+ class MessageGroupMixin(TimeStampMixin, AuditMixin):
76
+ """
77
+ This table is used to store the message group.
78
+ """
79
+ id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True, index=True)
80
+ name: Mapped[str] = mapped_column(nullable=False)
81
+ organization_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("organization.id"), nullable=False, index=True)
82
+
83
+ @declared_attr
84
+ def organization(cls) -> Mapped["Organization"]:
85
+ return relationship("Organization", back_populates="message_groups")
86
+
87
+ @declared_attr
88
+ def messages(cls) -> Mapped[List["Message"]]:
89
+ return relationship(back_populates="message_group")
90
+
91
+
92
+ class MessageGroupSync(CustomSyncBase, MessageGroupMixin):
93
+ pass
94
+
95
+
96
+ class MessageGroup(CustomBase, MessageGroupMixin):
97
+ pass
@@ -0,0 +1,78 @@
1
+ import uuid
2
+ from typing import Any, List
3
+
4
+ from slugify import slugify
5
+ from sqlalchemy import UUID, ForeignKey
6
+ from sqlalchemy.event import listen
7
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
8
+
9
+ from artemis_model.base import CustomSyncBase, TimeStampMixin, CustomBase
10
+
11
+ from sqlalchemy.ext.declarative import declared_attr
12
+
13
+
14
+ class OrganizationMixin(TimeStampMixin):
15
+ id: Mapped[uuid.UUID] = mapped_column(
16
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
17
+ )
18
+ name: Mapped[str] = mapped_column(nullable=False)
19
+ description: Mapped[str | None] = mapped_column(nullable=True)
20
+ slug: Mapped[str] = mapped_column(nullable=False, index=True)
21
+ disabled: Mapped[bool] = mapped_column(nullable=False, default=False)
22
+
23
+ @declared_attr
24
+ def locations(cls) -> Mapped[List["Location"]]:
25
+ return relationship(back_populates="organization")
26
+
27
+ @declared_attr
28
+ def dj_sets(cls) -> Mapped[List["DjSet"]]:
29
+ return relationship(back_populates="organization")
30
+
31
+ @declared_attr
32
+ def users(cls) -> Mapped[List["User"]]:
33
+ return relationship(
34
+ secondary="organization_user_assoc", back_populates="organizations"
35
+ )
36
+
37
+ @declared_attr
38
+ def messages(cls) -> Mapped[List["Message"]]:
39
+ return relationship(back_populates="organization")
40
+
41
+ @declared_attr
42
+ def message_groups(cls) -> Mapped[List["MessageGroup"]]:
43
+ return relationship(back_populates="organization")
44
+
45
+
46
+ def generate_slug(target: Any, value: Any, old_value: Any, initiator: Any) -> None:
47
+ """Creates a reasonable slug based on organization name."""
48
+ if value and (not target.slug or value != old_value):
49
+ target.slug = slugify(value, separator="_")
50
+
51
+
52
+ class OrganizationSync(CustomSyncBase, OrganizationMixin):
53
+ pass
54
+
55
+
56
+ listen(OrganizationSync.name, "set", generate_slug)
57
+
58
+
59
+ class Organization(CustomBase, OrganizationMixin):
60
+ pass
61
+
62
+
63
+ listen(Organization.name, "set", generate_slug)
64
+
65
+
66
+ class OrganizationUserAssocMixin(TimeStampMixin):
67
+ organization_id: Mapped[uuid.UUID] = mapped_column(
68
+ ForeignKey("organization.id"), primary_key=True
69
+ )
70
+ user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user.id"), primary_key=True)
71
+
72
+
73
+ class OrganizationUserAssocSync(CustomSyncBase, OrganizationUserAssocMixin):
74
+ pass
75
+
76
+
77
+ class OrganizationUserAssoc(CustomBase, OrganizationUserAssocMixin):
78
+ pass
artemis_model/otp.py ADDED
@@ -0,0 +1,34 @@
1
+
2
+ import uuid
3
+ from datetime import datetime
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+
6
+ from artemis_model.base import TimeStampMixin, CustomBase, CustomSyncBase
7
+
8
+
9
+ class OtpMixin(TimeStampMixin):
10
+ """
11
+ This table is used to store the OTPs.
12
+ requested_by can be an UserAccount or UserUnverifiedAccount id.
13
+ """
14
+
15
+ id: Mapped[int] = mapped_column(
16
+ primary_key=True, autoincrement=True, nullable=False
17
+ )
18
+ code: Mapped[str] = mapped_column(nullable=False)
19
+ purpose: Mapped[str] = mapped_column(nullable=False)
20
+ requested_by_id: Mapped[uuid.UUID] = mapped_column(nullable=False, index=True)
21
+ method: Mapped[str] = mapped_column(nullable=False, index=True)
22
+ secret: Mapped[str] = mapped_column(nullable=False)
23
+ created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, index=True)
24
+ updated_at: Mapped[datetime] = mapped_column(
25
+ default=datetime.utcnow, onupdate=datetime.utcnow
26
+ )
27
+
28
+
29
+ class OtpSync(CustomSyncBase, OtpMixin):
30
+ pass
31
+
32
+
33
+ class Otp(CustomBase, OtpMixin):
34
+ pass
@@ -0,0 +1,53 @@
1
+ import uuid
2
+ from typing import List
3
+
4
+ from sqlalchemy import ForeignKey
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+
7
+ from artemis_model.base import TimeStampMixin, CustomSyncBase, CustomBase
8
+
9
+ from sqlalchemy.ext.declarative import declared_attr
10
+
11
+
12
+ class PermissionMixin(TimeStampMixin):
13
+ id: Mapped[str] = mapped_column(
14
+ primary_key=True
15
+ )
16
+ name: Mapped[str] = mapped_column(nullable=False)
17
+ description: Mapped[str] = mapped_column(nullable=False)
18
+ type: Mapped[str] = mapped_column(nullable=False) # music-management, player-management, user-management
19
+
20
+ @declared_attr
21
+ def user_permission_associations(cls) -> Mapped[List["UserPermissionAssoc"]]:
22
+ return relationship(
23
+ back_populates="permission", cascade="all, delete-orphan"
24
+ )
25
+
26
+
27
+ class UserPermissionAssocMixin(TimeStampMixin):
28
+ user_id: Mapped[uuid.UUID] = mapped_column(
29
+ ForeignKey("user.id"), primary_key=True
30
+ )
31
+ permission_id: Mapped[uuid.UUID] = mapped_column(
32
+ ForeignKey("permission.id"), primary_key=True
33
+ )
34
+
35
+ @declared_attr
36
+ def permission(cls) -> Mapped["Permission"]:
37
+ return relationship(back_populates="user_permission_associations")
38
+
39
+
40
+ class UserPermissionAssocSync(CustomSyncBase, UserPermissionAssocMixin):
41
+ pass
42
+
43
+
44
+ class UserPermissionAssoc(CustomBase, UserPermissionAssocMixin):
45
+ pass
46
+
47
+
48
+ class PermissionSync(CustomSyncBase, PermissionMixin):
49
+ pass
50
+
51
+
52
+ class Permission(CustomBase, PermissionMixin):
53
+ pass
@@ -0,0 +1,191 @@
1
+ import uuid
2
+ from datetime import date, datetime
3
+
4
+ from sqlalchemy import Computed, ForeignKey, func, literal, text
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
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 PlaylistMixin(TimeStampMixin):
19
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
20
+ name: Mapped[str] = mapped_column(nullable=False, unique=True)
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
+ legacy_id: Mapped[str] = mapped_column(nullable=True)
26
+ is_published: Mapped[bool] = mapped_column(default=False) # legacy isTest
27
+ description: Mapped[str | None] = mapped_column(nullable=True)
28
+ cover_image: Mapped[str | None] = mapped_column(nullable=True)
29
+ is_ordered: Mapped[bool] = mapped_column(
30
+ default=False
31
+ ) # CanBeShuffled - Migration'da tersini al
32
+ legacy_prepared_by: Mapped[str | None] = mapped_column(nullable=True)
33
+ notes: Mapped[str | None] = mapped_column(nullable=True)
34
+ published_at: Mapped[date | None] = mapped_column(nullable=True)
35
+ is_private: Mapped[bool] = mapped_column(default=False)
36
+
37
+ name_tsv = mapped_column(
38
+ TSVector(),
39
+ Computed("to_tsvector('english', name)", persisted=True),
40
+ )
41
+
42
+ @declared_attr
43
+ def category_playlist_associations(cls) -> Mapped[
44
+ list["PlaylistCategoryAssoc"]
45
+ ]:
46
+ return relationship(cascade="all, delete-orphan")
47
+
48
+ @declared_attr
49
+ def track_playlist_associations(cls) -> Mapped[list["PlaylistTrackAssoc"]]:
50
+ return relationship(
51
+ cascade="all, delete-orphan"
52
+ )
53
+
54
+ @declared_attr
55
+ def organization_playlist_associations(cls) -> Mapped[
56
+ list["PlaylistOrganizationAssoc"]
57
+ ]:
58
+ return relationship(cascade="all, delete-orphan")
59
+
60
+ @declared_attr
61
+ def organization_ids(cls) -> AssociationProxy[list["Organization"] | None]:
62
+ return association_proxy(
63
+ "organization_playlist_associations",
64
+ "organization",
65
+ creator=lambda o: PlaylistOrganizationAssoc(organization_id=o),
66
+ )
67
+
68
+ @declared_attr
69
+ def category_ids(cls) -> AssociationProxy[list["Category"] | None]:
70
+ return association_proxy(
71
+ "category_playlist_associations",
72
+ "category",
73
+ creator=lambda c: PlaylistCategoryAssoc(category_id=c),
74
+ )
75
+
76
+ @declared_attr
77
+ def track_ids(cls) -> AssociationProxy[list["Track"] | None]:
78
+ return association_proxy(
79
+ "track_playlist_associations",
80
+ "track",
81
+ creator=lambda t: PlaylistTrackAssoc(track_id=t),
82
+ )
83
+
84
+ @declared_attr
85
+ def tracks(cls) -> Mapped[list["Track"]]:
86
+ return relationship(
87
+ secondary="playlist_track_assoc", viewonly=True
88
+ )
89
+
90
+ @declared_attr
91
+ def categories(cls) -> Mapped[list["Category"]]:
92
+ return relationship(
93
+ secondary="playlist_category_assoc", viewonly=True
94
+ )
95
+
96
+ @property
97
+ def total_duration(self) -> int:
98
+ return sum([0 if not t.duration else t.duration for t in self.tracks], 0)
99
+
100
+ # __table_args__ = (
101
+ # Index(
102
+ # "fts_ix_playlist_name_tsv",
103
+ # to_tsvector_ix("name"),
104
+ # postgresql_using="gin",
105
+ # ),
106
+ # )
107
+
108
+
109
+ class PlaylistSync(CustomSyncBase, PlaylistMixin):
110
+ pass
111
+
112
+
113
+ class Playlist(CustomBase, PlaylistMixin):
114
+ pass
115
+
116
+
117
+ class PlaylistOrganizationAssocMixin(TimeStampMixin):
118
+ playlist_id: Mapped[int] = mapped_column(
119
+ ForeignKey("playlist.id"), primary_key=True, nullable=False
120
+ )
121
+ organization_id: Mapped[uuid.UUID] = mapped_column(
122
+ ForeignKey("organization.id"), primary_key=True, nullable=False
123
+ )
124
+
125
+
126
+ class PlaylistOrganizationkAssocSync(CustomSyncBase, PlaylistOrganizationAssocMixin):
127
+ pass
128
+
129
+
130
+ class PlaylistOrganizationAssoc(CustomBase, PlaylistOrganizationAssocMixin):
131
+ pass
132
+
133
+
134
+ class PlaylistTrackAssocMixin(TimeStampMixin):
135
+ playlist_id: Mapped[int] = mapped_column(
136
+ ForeignKey("playlist.id"), primary_key=True, nullable=False
137
+ )
138
+ track_id: Mapped[uuid.UUID] = mapped_column(
139
+ ForeignKey("track.id"), primary_key=True, nullable=False
140
+ )
141
+
142
+ @declared_attr
143
+ def track(cls) -> Mapped["Track"]:
144
+ return relationship(back_populates="track_playlist_associations")
145
+
146
+
147
+ class PlaylistTrackAssocSync(CustomSyncBase, PlaylistTrackAssocMixin):
148
+ pass
149
+
150
+
151
+ class PlaylistTrackAssoc(CustomBase, PlaylistTrackAssocMixin):
152
+ pass
153
+
154
+
155
+ class PlaylistCategoryAssocMixin(TimeStampMixin):
156
+ playlist_id = mapped_column(
157
+ ForeignKey("playlist.id"), primary_key=True, nullable=False
158
+ )
159
+ category_id: Mapped[int] = mapped_column(
160
+ ForeignKey("category.id"), primary_key=True, nullable=False
161
+ )
162
+
163
+ @declared_attr
164
+ def category(cls) -> Mapped["Category"]:
165
+ return relationship(
166
+ back_populates="category_playlist_associations"
167
+ )
168
+
169
+
170
+ class PlaylistCategoryAssocSync(CustomSyncBase, PlaylistCategoryAssocMixin):
171
+ pass
172
+
173
+
174
+ class PlaylistCategoryAssoc(CustomBase, PlaylistCategoryAssocMixin):
175
+ pass
176
+
177
+
178
+ class PlaylistCounterMixin:
179
+ playlist_id: Mapped[int] = mapped_column(
180
+ ForeignKey("playlist.id"), primary_key=True, nullable=False
181
+ )
182
+ play_count: Mapped[int] = mapped_column(default=0)
183
+ schedule_count: Mapped[int] = mapped_column(default=0)
184
+
185
+
186
+ class PlaylistCounterSync(CustomSyncBase, PlaylistCounterMixin):
187
+ pass
188
+
189
+
190
+ class PlaylistCounter(CustomBase, PlaylistCounterMixin):
191
+ pass
@@ -0,0 +1,35 @@
1
+ import uuid
2
+
3
+ from sqlalchemy import ForeignKey
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+ from sqlalchemy.types import JSON, UUID
6
+
7
+ from artemis_model.base import CustomBase, TimeStampMixin, AuditMixin, CustomSyncBase
8
+
9
+ from sqlalchemy.ext.declarative import declared_attr
10
+
11
+
12
+ class ScheduleMixin(TimeStampMixin, AuditMixin):
13
+ """
14
+ This table is used to store the schedule of a zone.
15
+ """
16
+
17
+ id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True, index=True)
18
+ name: Mapped[str] = mapped_column(nullable=False)
19
+ zone_id: Mapped[int] = mapped_column(ForeignKey("zone.id"), index=True)
20
+ is_active: Mapped[bool] = mapped_column(default=True, nullable=False)
21
+ timesheet: Mapped[JSON] = mapped_column(JSON, nullable=True)
22
+ locked_by: Mapped[str] = mapped_column(default=None, nullable=True)
23
+ legacy_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=True)
24
+
25
+ @declared_attr
26
+ def zone(cls) -> Mapped["Zone"]:
27
+ return relationship(back_populates="schedules")
28
+
29
+
30
+ class ScheduleSync(CustomSyncBase, ScheduleMixin):
31
+ pass
32
+
33
+
34
+ class Schedule(CustomBase, ScheduleMixin):
35
+ pass