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/__init__.py +17 -0
- artemis_model/album.py +73 -0
- artemis_model/artist.py +51 -0
- artemis_model/auth.py +119 -0
- artemis_model/base.py +222 -0
- artemis_model/category.py +60 -0
- artemis_model/dj_set.py +120 -0
- artemis_model/genre.py +52 -0
- artemis_model/location.py +154 -0
- artemis_model/message.py +97 -0
- artemis_model/organization.py +78 -0
- artemis_model/otp.py +34 -0
- artemis_model/permission.py +53 -0
- artemis_model/playlist.py +191 -0
- artemis_model/schedule.py +35 -0
- artemis_model/setting.py +45 -0
- artemis_model/track.py +123 -0
- artemis_model/user.py +73 -0
- artemis_model/zone.py +69 -0
- artemis_model-0.1.66.dist-info/METADATA +113 -0
- artemis_model-0.1.66.dist-info/RECORD +22 -0
- artemis_model-0.1.66.dist-info/WHEEL +4 -0
@@ -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
|
artemis_model/artist.py
ADDED
@@ -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
|
artemis_model/dj_set.py
ADDED
@@ -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
|