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 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
@@ -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
+
@@ -35,4 +35,4 @@ class LocationGenreExclusionSync(CustomSyncBase, LocationGenreExclusionMixin):
35
35
 
36
36
 
37
37
  class LocationGenreExclusion(CustomBase, LocationGenreExclusionMixin):
38
- pass
38
+ pass
@@ -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(back_populates="organization", uselist=False, cascade="all, delete-orphan")
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"]:
@@ -3,7 +3,7 @@
3
3
  from datetime import datetime
4
4
  from typing import Annotated, Literal
5
5
 
6
- from pydantic import BaseModel, Field, field_validator
6
+ from pydantic import BaseModel, Field
7
7
 
8
8
 
9
9
  class NowPlaying(BaseModel):
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: artemis-model
3
- Version: 0.1.164
3
+ Version: 0.1.189
4
4
  Summary:
5
5
  Author: Jukeboxy
6
6
  Requires-Python: >=3.10.6,<4.0.0
@@ -1,35 +1,38 @@
1
- artemis_model/__init__.py,sha256=R__YcDg7FMAMHyy9CVjqi6fC58DEWkwMXCg0S9Az6Ck,981
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=JJ2n15JA7kBbsKrQA-GpfriMEjCHZmWcFhFAKfNcPxY,1869
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=0Yg_6N1nj4U31F86u1PE0leIa3D64QlYrsDlavulN6s,4033
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=SMX4TXmpwn28tytUq7-qP9ZyJ_ULs5SUv4h7sU0dRFo,1252
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=oyq8YIqVr5HAli1IuqFj2UlVKw9KBpWIMlZGcEzmYyA,3167
15
- artemis_model/organization_include_pal_setting.py,sha256=OhNRn4aH96IquJcJBsubvZEgIY3ENqPr4Rda4GWEZ1g,1179
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=O60zlP1LrDkzNrrfNA7TD_kotd5A9i8PuFhwonXFhN0,6559
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=A5z2ss1-zq6_UHavs6mBgxsllXLz5hYbKCNRAjD-ONw,3152
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=2TVlSQJzF2phblQGrlUWdQ8Uw-kJstnGtcbY06MIBWM,2852
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-0.1.164.dist-info/METADATA,sha256=5LWzOZJppKC3dEjfYtbpLTHFtZRq3CFh_giM1QgcIbE,3496
34
- artemis_model-0.1.164.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
35
- artemis_model-0.1.164.dist-info/RECORD,,
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,,