artemis-model 0.1.164__py3-none-any.whl → 0.1.189__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 +3 -0
- artemis_model/approved_playlist_list.py +1 -5
- artemis_model/auth.py +80 -1
- artemis_model/billing.py +180 -0
- artemis_model/location_genre_exclusion.py +1 -1
- artemis_model/organization.py +12 -1
- artemis_model/organization_include_pal_setting.py +2 -4
- artemis_model/playlist.py +2 -0
- artemis_model/redis/zone_state.py +1 -1
- artemis_model/sqs/messages.py +29 -0
- artemis_model/sync_state.py +47 -0
- artemis_model/zone_state.py +88 -0
- {artemis_model-0.1.164.dist-info → artemis_model-0.1.189.dist-info}/METADATA +1 -1
- {artemis_model-0.1.164.dist-info → artemis_model-0.1.189.dist-info}/RECORD +15 -12
- {artemis_model-0.1.164.dist-info → artemis_model-0.1.189.dist-info}/WHEEL +0 -0
artemis_model/__init__.py
CHANGED
|
@@ -13,9 +13,12 @@ from artemis_model.organization_include_pal_setting import * # noqa
|
|
|
13
13
|
from artemis_model.playlist import * # noqa
|
|
14
14
|
from artemis_model.schedule import * # noqa
|
|
15
15
|
from artemis_model.setting import * # noqa
|
|
16
|
+
from artemis_model.sync_state import * # noqa
|
|
16
17
|
from artemis_model.track import * # noqa
|
|
17
18
|
from artemis_model.user import * # noqa
|
|
18
19
|
from artemis_model.zone import * # noqa
|
|
19
20
|
from artemis_model.otp import * # noqa
|
|
20
21
|
from artemis_model.banned_tracks import * # noqa
|
|
21
22
|
from artemis_model.zone_activity import * # noqa
|
|
23
|
+
from artemis_model.zone_state import * # noqa
|
|
24
|
+
from artemis_model.billing import * # noqa
|
|
@@ -9,7 +9,6 @@ from artemis_model.base import CustomSyncBase, TimeStampMixin, AuditMixin, Custo
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ApprovedPlaylistListMixin(TimeStampMixin, AuditMixin):
|
|
12
|
-
|
|
13
12
|
id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True, index=True)
|
|
14
13
|
name: Mapped[str] = mapped_column(nullable=False)
|
|
15
14
|
organization_id: Mapped[uuid.UUID] = mapped_column(
|
|
@@ -27,9 +26,7 @@ class ApprovedPlaylistListMixin(TimeStampMixin, AuditMixin):
|
|
|
27
26
|
@declared_attr
|
|
28
27
|
def playlists(cls) -> Mapped[List["Playlist"]]:
|
|
29
28
|
return relationship(
|
|
30
|
-
"Playlist",
|
|
31
|
-
secondary="approved_playlist_list_playlist_assoc",
|
|
32
|
-
viewonly=True
|
|
29
|
+
"Playlist", secondary="approved_playlist_list_playlist_assoc", viewonly=True
|
|
33
30
|
)
|
|
34
31
|
|
|
35
32
|
|
|
@@ -42,7 +39,6 @@ class ApprovedPlaylistList(CustomBase, ApprovedPlaylistListMixin):
|
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
class ApprovedPlaylistListPlaylistAssocMixin(TimeStampMixin):
|
|
45
|
-
|
|
46
42
|
approved_playlist_list_id: Mapped[int] = mapped_column(
|
|
47
43
|
ForeignKey("approved_playlist_list.id"), primary_key=True, nullable=False
|
|
48
44
|
)
|
artemis_model/auth.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"""Auth models."""
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
3
4
|
import uuid
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
8
|
+
from pydantic import BaseModel
|
|
7
9
|
from sqlalchemy import UUID, DateTime, ForeignKey, LargeBinary
|
|
8
10
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
11
|
from sqlalchemy.ext.declarative import declared_attr
|
|
10
12
|
|
|
11
|
-
from artemis_model.base import CustomSyncBase, TimeStampMixin, CustomBase
|
|
13
|
+
from artemis_model.base import AuditMixin, CustomSyncBase, TimeStampMixin, CustomBase
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class UserUnverifiedAccountMixin(TimeStampMixin):
|
|
@@ -111,3 +113,80 @@ class OAuthCsrfStateSync(CustomSyncBase, OAuthCsrfStateMixin):
|
|
|
111
113
|
|
|
112
114
|
class OAuthCsrfState(CustomBase, OAuthCsrfStateMixin):
|
|
113
115
|
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class OrionWebplayerCodeMixin(TimeStampMixin, AuditMixin):
|
|
119
|
+
"""Orion webplayer code mixin."""
|
|
120
|
+
|
|
121
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
122
|
+
code_hash: Mapped[str] = mapped_column(
|
|
123
|
+
unique=True, nullable=False
|
|
124
|
+
) # Argon2id of normalized code
|
|
125
|
+
name: Mapped[str] = mapped_column(nullable=False)
|
|
126
|
+
zone_id: Mapped[int] = mapped_column(nullable=False, index=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class OrionWebplayerCodeSync(CustomSyncBase, OrionWebplayerCodeMixin):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class OrionWebplayerCode(CustomBase, OrionWebplayerCodeMixin):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PlayerRefreshTokenMixin(TimeStampMixin):
|
|
138
|
+
"""
|
|
139
|
+
Refresh token for Player.
|
|
140
|
+
We look up by token_id (PK), then verify `secret` against `secret_hash`.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
# this is the public part we send to the client
|
|
144
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
145
|
+
UUID(as_uuid=True),
|
|
146
|
+
primary_key=True,
|
|
147
|
+
default=uuid.uuid4,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
code_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
151
|
+
UUID(as_uuid=True),
|
|
152
|
+
ForeignKey("orion_webplayer_code.id", ondelete="SET NULL"),
|
|
153
|
+
index=True,
|
|
154
|
+
nullable=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# store zone_id too (helps auth decisions / debugging)
|
|
158
|
+
zone_id: Mapped[Optional[int]] = mapped_column(index=True)
|
|
159
|
+
|
|
160
|
+
# bcrypt/argon2 hash of the secret part
|
|
161
|
+
secret_hash: Mapped[str] = mapped_column(nullable=False)
|
|
162
|
+
is_suspended: Mapped[bool] = mapped_column(default=False)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PlayerRefreshTokenSync(CustomSyncBase, PlayerRefreshTokenMixin):
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class PlayerRefreshToken(CustomBase, PlayerRefreshTokenMixin):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Scope(str, Enum):
|
|
174
|
+
"""Scope enum."""
|
|
175
|
+
|
|
176
|
+
PLAYER = "player"
|
|
177
|
+
MANAGE = "manage"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TokenData(BaseModel):
|
|
181
|
+
"""Token data."""
|
|
182
|
+
|
|
183
|
+
account_id: uuid.UUID
|
|
184
|
+
user_id: uuid.UUID
|
|
185
|
+
scope: Scope
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class RefreshTokenData(TokenData):
|
|
189
|
+
"""Refresh token data."""
|
|
190
|
+
|
|
191
|
+
zone_id: int | None = None
|
|
192
|
+
code_id: uuid.UUID | None = None
|
artemis_model/billing.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import UUID, ForeignKey
|
|
6
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
8
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
9
|
+
|
|
10
|
+
from artemis_model.base import CustomBase, CustomSyncBase, TimeStampMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrganizationBillingMixin(TimeStampMixin):
|
|
14
|
+
organization_id: Mapped[uuid.UUID] = mapped_column(
|
|
15
|
+
UUID(as_uuid=True), ForeignKey("organization.id"), primary_key=True
|
|
16
|
+
)
|
|
17
|
+
stripe_customer_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
|
18
|
+
|
|
19
|
+
# Single active subscription reference (for org-level billing). When per-location
|
|
20
|
+
# subscriptions are used, this may be null and looked up from OrganizationSubscription.
|
|
21
|
+
stripe_subscription_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
|
22
|
+
subscription_status: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
23
|
+
billing_cycle: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
24
|
+
trial_end: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
25
|
+
current_period_start: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
26
|
+
current_period_end: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
27
|
+
default_payment_method_id: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
28
|
+
|
|
29
|
+
@declared_attr
|
|
30
|
+
def organization(cls) -> Mapped["Organization"]:
|
|
31
|
+
return relationship("Organization", back_populates="billing", uselist=False)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OrganizationBilling(CustomBase, OrganizationBillingMixin):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OrganizationBillingSync(CustomSyncBase, OrganizationBillingMixin):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OrganizationSubscriptionMixin(TimeStampMixin):
|
|
43
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
44
|
+
organization_id: Mapped[uuid.UUID] = mapped_column(
|
|
45
|
+
UUID(as_uuid=True), ForeignKey("organization.id"), index=True
|
|
46
|
+
)
|
|
47
|
+
stripe_subscription_id: Mapped[str] = mapped_column(index=True)
|
|
48
|
+
status: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
49
|
+
billing_cycle: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
50
|
+
trial_end: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
51
|
+
current_period_start: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
52
|
+
current_period_end: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
53
|
+
# Pending cycle change metadata (scheduled to flip at period end)
|
|
54
|
+
pending_billing_cycle: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
55
|
+
pending_billing_cycle_effective_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
56
|
+
stripe_schedule_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
|
57
|
+
# Primary scope for this subscription: 'organization' or 'location'
|
|
58
|
+
scope_type: Mapped[str] = mapped_column(default="organization")
|
|
59
|
+
scope_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
60
|
+
|
|
61
|
+
@declared_attr
|
|
62
|
+
def organization(cls) -> Mapped["Organization"]:
|
|
63
|
+
return relationship("Organization", back_populates="subscriptions")
|
|
64
|
+
|
|
65
|
+
@declared_attr
|
|
66
|
+
def items(cls) -> Mapped[list["OrganizationSubscriptionItem"]]:
|
|
67
|
+
return relationship(
|
|
68
|
+
"OrganizationSubscriptionItem", back_populates="subscription", cascade="all, delete-orphan"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OrganizationSubscription(CustomBase, OrganizationSubscriptionMixin):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OrganizationSubscriptionSync(CustomSyncBase, OrganizationSubscriptionMixin):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class OrganizationSubscriptionItemMixin(TimeStampMixin):
|
|
81
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
82
|
+
organization_subscription_id: Mapped[uuid.UUID] = mapped_column(
|
|
83
|
+
UUID(as_uuid=True), ForeignKey("organization_subscription.id"), index=True
|
|
84
|
+
)
|
|
85
|
+
stripe_subscription_item_id: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True)
|
|
86
|
+
product_type: Mapped[str] = mapped_column() # 'location' | 'additional_zone' | ...
|
|
87
|
+
stripe_price_id: Mapped[str] = mapped_column()
|
|
88
|
+
quantity: Mapped[int] = mapped_column()
|
|
89
|
+
scope_type: Mapped[str] = mapped_column(default="organization") # 'organization' | 'location'
|
|
90
|
+
scope_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
91
|
+
metadata_json: Mapped[Optional[dict]] = mapped_column(JSONB, name="metadata", nullable=True)
|
|
92
|
+
|
|
93
|
+
@declared_attr
|
|
94
|
+
def subscription(cls) -> Mapped["OrganizationSubscription"]:
|
|
95
|
+
return relationship("OrganizationSubscription", back_populates="items")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class OrganizationSubscriptionItem(CustomBase, OrganizationSubscriptionItemMixin):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OrganizationSubscriptionItemSync(CustomSyncBase, OrganizationSubscriptionItemMixin):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BillingChangeLogMixin(TimeStampMixin):
|
|
107
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
108
|
+
organization_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
|
|
109
|
+
organization_subscription_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
110
|
+
UUID(as_uuid=True), nullable=True, index=True
|
|
111
|
+
)
|
|
112
|
+
organization_subscription_item_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
113
|
+
UUID(as_uuid=True), nullable=True, index=True
|
|
114
|
+
)
|
|
115
|
+
change_type: Mapped[str] = mapped_column() # e.g., 'subscription_created', 'quantity_updated', 'price_changed'
|
|
116
|
+
old_value_json: Mapped[Optional[dict]] = mapped_column(JSONB, name="old_value", nullable=True)
|
|
117
|
+
new_value_json: Mapped[Optional[dict]] = mapped_column(JSONB, name="new_value", nullable=True)
|
|
118
|
+
reason: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
119
|
+
actor_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class BillingChangeLog(CustomBase, BillingChangeLogMixin):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BillingChangeLogSync(CustomSyncBase, BillingChangeLogMixin):
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class PaymentLogMixin(TimeStampMixin):
|
|
131
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
132
|
+
organization_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
|
|
133
|
+
stripe_invoice_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
|
134
|
+
amount: Mapped[Optional[int]] = mapped_column(nullable=True) # cents
|
|
135
|
+
amount_major: Mapped[Optional[float]] = mapped_column(nullable=True) # dollars (e.g., 18.10)
|
|
136
|
+
currency: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
137
|
+
status: Mapped[Optional[str]] = mapped_column(nullable=True) # paid, open, void, uncollectible
|
|
138
|
+
occurred_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
139
|
+
raw: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) # webhook payload snapshot
|
|
140
|
+
# One-time payments / checkout tracking
|
|
141
|
+
type: Mapped[Optional[str]] = mapped_column(nullable=True) # 'subscription_invoice' | 'one_time' | 'checkout'
|
|
142
|
+
stripe_payment_intent_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
|
143
|
+
stripe_checkout_session_id: Mapped[Optional[str]] = mapped_column(nullable=True, index=True)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class PaymentLog(CustomBase, PaymentLogMixin):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PaymentLogSync(CustomSyncBase, PaymentLogMixin):
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class MusicManageAddonMixin(TimeStampMixin):
|
|
155
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
156
|
+
scope_type: Mapped[str] = mapped_column() # 'organization' | 'location'
|
|
157
|
+
organization_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
|
|
158
|
+
location_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)
|
|
159
|
+
|
|
160
|
+
active: Mapped[bool] = mapped_column(default=True)
|
|
161
|
+
cancel_at_period_end: Mapped[bool] = mapped_column(default=False)
|
|
162
|
+
quantity: Mapped[int] = mapped_column(default=0)
|
|
163
|
+
|
|
164
|
+
stripe_subscription_item_id: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True)
|
|
165
|
+
stripe_price_id: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
166
|
+
|
|
167
|
+
subscription_status: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
168
|
+
billing_cycle: Mapped[Optional[str]] = mapped_column(nullable=True)
|
|
169
|
+
current_period_start: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
170
|
+
current_period_end: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MusicManageAddon(CustomBase, MusicManageAddonMixin):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class MusicManageAddonSync(CustomSyncBase, MusicManageAddonMixin):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
artemis_model/organization.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
from typing import Any, List
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from slugify import slugify
|
|
5
6
|
from sqlalchemy import UUID, ForeignKey
|
|
@@ -37,10 +38,20 @@ class OrganizationMixin(TimeStampMixin):
|
|
|
37
38
|
@declared_attr
|
|
38
39
|
def message_groups(cls) -> Mapped[List["MessageGroup"]]:
|
|
39
40
|
return relationship(back_populates="organization")
|
|
41
|
+
|
|
42
|
+
@declared_attr
|
|
43
|
+
def billing(cls) -> Mapped[Optional["OrganizationBilling"]]:
|
|
44
|
+
return relationship("OrganizationBilling", back_populates="organization", uselist=False)
|
|
45
|
+
|
|
46
|
+
@declared_attr
|
|
47
|
+
def subscriptions(cls) -> Mapped[List["OrganizationSubscription"]]:
|
|
48
|
+
return relationship("OrganizationSubscription", back_populates="organization")
|
|
40
49
|
|
|
41
50
|
@declared_attr
|
|
42
51
|
def include_pal_setting(cls) -> Mapped["OrganizationIncludePalSetting"]:
|
|
43
|
-
return relationship(
|
|
52
|
+
return relationship(
|
|
53
|
+
back_populates="organization", uselist=False, cascade="all, delete-orphan"
|
|
54
|
+
)
|
|
44
55
|
|
|
45
56
|
@declared_attr
|
|
46
57
|
def approved_playlist_lists(cls) -> Mapped[List["ApprovedPlaylistList"]]:
|
|
@@ -13,9 +13,7 @@ class OrganizationIncludePalSettingMixin(TimeStampMixin):
|
|
|
13
13
|
organization_id: Mapped[uuid.UUID] = mapped_column(
|
|
14
14
|
ForeignKey("organization.id", ondelete="CASCADE"), nullable=False, index=True
|
|
15
15
|
)
|
|
16
|
-
include_pal: Mapped[bool] = mapped_column(
|
|
17
|
-
nullable=False, default=False
|
|
18
|
-
)
|
|
16
|
+
include_pal: Mapped[bool] = mapped_column(nullable=False, default=False)
|
|
19
17
|
|
|
20
18
|
@declared_attr
|
|
21
19
|
def organization(cls) -> Mapped["Organization"]:
|
|
@@ -31,4 +29,4 @@ class OrganizationIncludePalSettingSync(CustomSyncBase, OrganizationIncludePalSe
|
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
class OrganizationIncludePalSetting(CustomBase, OrganizationIncludePalSettingMixin):
|
|
34
|
-
pass
|
|
32
|
+
pass
|
artemis_model/playlist.py
CHANGED
|
@@ -149,6 +149,8 @@ class PlaylistTrackAssocMixin(TimeStampMixin):
|
|
|
149
149
|
track_id: Mapped[uuid.UUID] = mapped_column(
|
|
150
150
|
ForeignKey("track.id"), primary_key=True, nullable=False
|
|
151
151
|
)
|
|
152
|
+
legacy_playlist_id: Mapped[str] = mapped_column(nullable=True)
|
|
153
|
+
legacy_track_id: Mapped[str] = mapped_column(nullable=True)
|
|
152
154
|
|
|
153
155
|
@declared_attr
|
|
154
156
|
def track(cls) -> Mapped["Track"]:
|
artemis_model/sqs/messages.py
CHANGED
|
@@ -26,6 +26,9 @@ class Action(str, Enum):
|
|
|
26
26
|
PLAYER_MODE_CHANGE = "player-mode-change"
|
|
27
27
|
BAN_TRACK = "ban-track"
|
|
28
28
|
ZONE_ACTIVITY = "zone-activity"
|
|
29
|
+
STOP_MUSIC = "stop-music"
|
|
30
|
+
EXPIRE_LICENCE = "expire-licence"
|
|
31
|
+
REMOVE_ACTIVE_DEVICE = "remove-active-device"
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class BaseMessage(BaseModel):
|
|
@@ -47,6 +50,13 @@ class MoveTimeSlotIn(BaseMessage):
|
|
|
47
50
|
playlist_ids: list[int]
|
|
48
51
|
|
|
49
52
|
|
|
53
|
+
class StopMusicIn(BaseMessage):
|
|
54
|
+
"""Stop music message schema."""
|
|
55
|
+
|
|
56
|
+
action: Action = Action.STOP_MUSIC
|
|
57
|
+
zone_id: int
|
|
58
|
+
|
|
59
|
+
|
|
50
60
|
class TriageScheduleIn(BaseMessage):
|
|
51
61
|
"""Triage schedule message schema."""
|
|
52
62
|
|
|
@@ -112,3 +122,22 @@ class ZoneActivityIn(BaseMessage):
|
|
|
112
122
|
zone_id: int
|
|
113
123
|
activity_type: ZoneActivityType
|
|
114
124
|
activity_data: dict
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ExpireLicenceIn(BaseMessage):
|
|
128
|
+
"""Expire licence message schema."""
|
|
129
|
+
|
|
130
|
+
action: Action = Action.EXPIRE_LICENCE
|
|
131
|
+
organization_id: UUID
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class RemoveActiveDeviceIn(BaseMessage):
|
|
135
|
+
"""Remove active device message schema."""
|
|
136
|
+
|
|
137
|
+
action: Action = Action.REMOVE_ACTIVE_DEVICE
|
|
138
|
+
zone_id: int
|
|
139
|
+
device_id: str
|
|
140
|
+
reason: str = Field(
|
|
141
|
+
default="websocket_disconnect",
|
|
142
|
+
description="Reason for device removal (websocket_disconnect, shutdown, etc.)",
|
|
143
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Sync state model for tracking data syncer progress."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from artemis_model.base import CustomBase, CustomSyncBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SyncStateMixin:
|
|
12
|
+
"""Mixin for sync state tracking."""
|
|
13
|
+
|
|
14
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
15
|
+
table_name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True)
|
|
16
|
+
last_sync_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
|
17
|
+
last_successful_sync_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
|
18
|
+
total_synced: Mapped[int] = mapped_column(nullable=False, default=0)
|
|
19
|
+
total_failed: Mapped[int] = mapped_column(nullable=False, default=0)
|
|
20
|
+
status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
|
|
21
|
+
last_error: Mapped[str | None] = mapped_column(nullable=True)
|
|
22
|
+
created_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow)
|
|
23
|
+
updated_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SyncStateSync(CustomSyncBase, SyncStateMixin):
|
|
27
|
+
"""Sync version of SyncState model."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SyncState(CustomBase, SyncStateMixin):
|
|
33
|
+
"""
|
|
34
|
+
Tracks the sync state for data syncer operations.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
table_name: Name of the source table being synced
|
|
38
|
+
last_sync_at: Timestamp of last sync attempt
|
|
39
|
+
last_successful_sync_at: Timestamp of last successful sync (used for incremental sync)
|
|
40
|
+
total_synced: Cumulative count of records synced
|
|
41
|
+
total_failed: Cumulative count of failed records
|
|
42
|
+
status: Current status (pending, running, completed, failed)
|
|
43
|
+
last_error: Last error message if any
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Zone state data models."""
|
|
2
|
+
|
|
3
|
+
# models/zone_state.py
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import DateTime, ForeignKey, Integer, String
|
|
10
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
11
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
12
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
13
|
+
|
|
14
|
+
from artemis_model.base import CustomSyncBase, TimeStampMixin, CustomBase
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ZoneStateMetaMixin(TimeStampMixin):
|
|
18
|
+
"""
|
|
19
|
+
Rarely changing part of a zone's state.
|
|
20
|
+
One row per zone.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
zone_id: Mapped[int] = mapped_column(
|
|
24
|
+
Integer,
|
|
25
|
+
ForeignKey("zone.id", ondelete="CASCADE"),
|
|
26
|
+
primary_key=True,
|
|
27
|
+
default=uuid.uuid4,
|
|
28
|
+
doc="Zone identifier (PK)",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
player_mode: Mapped[str] = mapped_column(
|
|
32
|
+
String, default="scheduled", nullable=False, index=True
|
|
33
|
+
)
|
|
34
|
+
player_state: Mapped[str] = mapped_column(String, default="ready", nullable=False, index=True)
|
|
35
|
+
pp_details: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
|
36
|
+
schedule_details: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
|
37
|
+
|
|
38
|
+
@declared_attr
|
|
39
|
+
def now_playing(cls) -> Mapped["ZoneNowPlaying"]:
|
|
40
|
+
return relationship(
|
|
41
|
+
"ZoneNowPlaying",
|
|
42
|
+
back_populates="meta",
|
|
43
|
+
uselist=False,
|
|
44
|
+
cascade="all, delete-orphan",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ZoneStateMetaSync(CustomSyncBase, ZoneStateMetaMixin):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ZoneStateMeta(CustomBase, ZoneStateMetaMixin):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ZoneNowPlayingMixin:
|
|
57
|
+
"""
|
|
58
|
+
Frequently changing part of a zone's state.
|
|
59
|
+
Keep row narrow; PK-only index for cheap updates.
|
|
60
|
+
One row per zone (FK to ZoneStateMeta).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
zone_id: Mapped[int] = mapped_column(
|
|
64
|
+
Integer,
|
|
65
|
+
ForeignKey("zone_state_meta.zone_id", ondelete="CASCADE"),
|
|
66
|
+
primary_key=True,
|
|
67
|
+
doc="Matches zone_state_meta.zone_id",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
track_name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
71
|
+
artist_name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
72
|
+
album_name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
73
|
+
playlist_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
74
|
+
|
|
75
|
+
# Lightweight timestamp for freshness; mirrors your style (see LoginHistory)
|
|
76
|
+
updated_at = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
77
|
+
|
|
78
|
+
@declared_attr
|
|
79
|
+
def meta(cls) -> Mapped["ZoneStateMeta"]:
|
|
80
|
+
return relationship("ZoneStateMeta", back_populates="now_playing")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ZoneNowPlayingSync(CustomSyncBase, ZoneNowPlayingMixin):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ZoneNowPlaying(CustomBase, ZoneNowPlayingMixin):
|
|
88
|
+
pass
|
|
@@ -1,35 +1,38 @@
|
|
|
1
|
-
artemis_model/__init__.py,sha256=
|
|
1
|
+
artemis_model/__init__.py,sha256=GwR2VYyOKvEt-Xwi0dWn11Nt9lLx8Wt4PLXkFPe2nT4,1118
|
|
2
2
|
artemis_model/album.py,sha256=9uw9HVNHVBjl-0Dgv-o5MHXhUPwedvbnbzzY3A1cKQg,2077
|
|
3
|
-
artemis_model/approved_playlist_list.py,sha256=
|
|
3
|
+
artemis_model/approved_playlist_list.py,sha256=Tf8yB778tsnoYc7xBhcJzNA6ZfMQBanEy0aDKL5ReeA,1843
|
|
4
4
|
artemis_model/artist.py,sha256=vjXlFN2mOEidAzUzxmsAP5GnVTvJUuhFdhQaH214lgw,1563
|
|
5
|
-
artemis_model/auth.py,sha256=
|
|
5
|
+
artemis_model/auth.py,sha256=ErDorUfG32TqNYVqDidDg1LJwUldiqru1J7PEoNjOfk,6049
|
|
6
6
|
artemis_model/banned_tracks.py,sha256=uU-F-6DL2EshPAUwLTTHjYZ7UEz4vm0Wfcif2F0lSKw,664
|
|
7
7
|
artemis_model/base.py,sha256=zC20m8a1Sa11oEY0ay1iIRQeybGYvXY5p3Vr-pxoKnQ,6802
|
|
8
|
+
artemis_model/billing.py,sha256=HUGJxyqgfTTCkX9m5anmLx8x-84kHNOsbDEEgbdbv_I,8518
|
|
8
9
|
artemis_model/category.py,sha256=ERZC8YBTtmF72ykSiVEtf_Ws3mPuN28ECfTxWF7H8tE,1662
|
|
9
10
|
artemis_model/dj_set.py,sha256=fOYnCu4n5TiqyiSojfdFnO7LuPe_mM2SUwBV5xHy2Kc,3782
|
|
10
11
|
artemis_model/genre.py,sha256=8_-IuJS543wIhUVCES3bdrDpKPKx-plDuBKGBcoMIbc,1570
|
|
11
12
|
artemis_model/location.py,sha256=6Z99OCxhB3VQ4CqNwZP3ShnJ-gOnc5rxGnCn5aCIFZ8,4896
|
|
12
|
-
artemis_model/location_genre_exclusion.py,sha256=
|
|
13
|
+
artemis_model/location_genre_exclusion.py,sha256=BUyvenXFctvTAJX6GsNESy3llwmiXMjvle2BCzO_KYo,1253
|
|
13
14
|
artemis_model/message.py,sha256=W4vhllsD4Nn11JIKeXlgsKC2NWCt3UMkWh-Sma71gBI,3325
|
|
14
|
-
artemis_model/organization.py,sha256=
|
|
15
|
-
artemis_model/organization_include_pal_setting.py,sha256=
|
|
15
|
+
artemis_model/organization.py,sha256=B4JX7QN4fFGy95nfdWdh8xAdEbz8KhNESuoJKHpi_q8,3578
|
|
16
|
+
artemis_model/organization_include_pal_setting.py,sha256=w9Uqy5n-lVraJYGconuQP82J1Us1-5gv4O7CkgVZuz8,1166
|
|
16
17
|
artemis_model/otp.py,sha256=guIRGtyFlHUBthCAEsTh5_Hs-1yiGN_qfEO4uHNcv4s,1017
|
|
17
18
|
artemis_model/permission.py,sha256=Bn1Bg1aCS4Z4_3tqEqvtrzqAYDCImsvmGyIEMoVycEk,1452
|
|
18
|
-
artemis_model/playlist.py,sha256=
|
|
19
|
+
artemis_model/playlist.py,sha256=XqIZ_CL_N0GbTmkvOlmtjfx5BBVZ0uLtQyJB4IQLlvQ,6690
|
|
19
20
|
artemis_model/redis/__init__.py,sha256=hwxNaoTAwFWJxHL6ijp5MOJkapWPv9PEPoY6rtUhCyI,944
|
|
20
21
|
artemis_model/redis/bucket.py,sha256=DlmIf6GxfKq9CzcXmMx5IcviaqOTvuWwIb6lAVcZQGs,1537
|
|
21
22
|
artemis_model/redis/device.py,sha256=MmCIpBmWxm80CHeZinAXYZOdtPE49xXLo9oRa4FyjcY,505
|
|
22
23
|
artemis_model/redis/keys.py,sha256=X5lvVJHeKHt0lJKhi5i8KzNtDGQ3TuGrjm3V2CcTgXw,674
|
|
23
24
|
artemis_model/redis/play_history.py,sha256=Jm0guS0UZDxfCXeWJ8vqcjjl93W_EeC7XcBXcclKPiE,1259
|
|
24
|
-
artemis_model/redis/zone_state.py,sha256=
|
|
25
|
+
artemis_model/redis/zone_state.py,sha256=IjzRuUD--mNBBPU4gU1T6ika06TU64rbjPD_vDlH1IM,3135
|
|
25
26
|
artemis_model/schedule.py,sha256=CkLHWz-BwvUY2EQCfoU4SymgCariPzoQdtRLITqBPmk,2451
|
|
26
27
|
artemis_model/setting.py,sha256=xe5SHDziY8RzFxzB1GzousxI1FXYhyXZ5proEseS60g,1190
|
|
27
28
|
artemis_model/sqs/__init__.py,sha256=nHpXQns64qQ5Cqjyo6w9fDGO_wWhprqn1bhKf3eWnio,17
|
|
28
|
-
artemis_model/sqs/messages.py,sha256=
|
|
29
|
+
artemis_model/sqs/messages.py,sha256=fyGsMGMem6IYDByhIWe8drQjYh5JUufWcXhy3ryEqgc,3582
|
|
30
|
+
artemis_model/sync_state.py,sha256=Ykn0jGqEjlCdZeopznSPwPEiZPgUeAsxZBFVGsH8D7U,1778
|
|
29
31
|
artemis_model/track.py,sha256=QwUF0QKVn1I64648B-NI75-IzGQvnLt9B0emD4GnS6E,3757
|
|
30
32
|
artemis_model/user.py,sha256=eqIdCiBJRNLjCwPPCn-gQ6si0O5JUBGfp9oWJL5zVW4,2131
|
|
31
33
|
artemis_model/zone.py,sha256=iGRUtzUwKh9LHT3MOfzzg1DnkPBts_ZBzZVTi2EmIgs,2282
|
|
32
34
|
artemis_model/zone_activity.py,sha256=BY4iODavY9ceJ5oRChdjjxf26S3U30Yb7Pxm5YRFpCo,1590
|
|
33
|
-
artemis_model
|
|
34
|
-
artemis_model-0.1.
|
|
35
|
-
artemis_model-0.1.
|
|
35
|
+
artemis_model/zone_state.py,sha256=9cKhQFigYmKkfXq1E7OooO0hQciwd6cUTl033PPNugU,2673
|
|
36
|
+
artemis_model-0.1.189.dist-info/METADATA,sha256=EgOBPVdT5AaU3XyUmgnKIbBL65TvL0YhlDX7wX24WzU,3496
|
|
37
|
+
artemis_model-0.1.189.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
38
|
+
artemis_model-0.1.189.dist-info/RECORD,,
|
|
File without changes
|