Skip to content

Commit

Permalink
Add LiveActivity support
Browse files Browse the repository at this point in the history
  • Loading branch information
nnsnodnb committed Jan 13, 2024
1 parent 0706f10 commit 02440fb
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 18 deletions.
31 changes: 21 additions & 10 deletions kalyke/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
from .clients.apns import ApnsClient

Check failure on line 1 in kalyke/__init__.py

View workflow job for this annotation

GitHub Actions / isort (3.9)

Imports are incorrectly sorted and/or formatted.

Check failure on line 1 in kalyke/__init__.py

View workflow job for this annotation

GitHub Actions / isort (3.10)

Imports are incorrectly sorted and/or formatted.

Check failure on line 1 in kalyke/__init__.py

View workflow job for this annotation

GitHub Actions / isort (3.11)

Imports are incorrectly sorted and/or formatted.

Check failure on line 1 in kalyke/__init__.py

View workflow job for this annotation

GitHub Actions / isort (3.12)

Imports are incorrectly sorted and/or formatted.
from .clients.live_activity import LiveActivityClient
from .clients.voip import VoIPClient
from .models.apns_config import ApnsConfig, LiveActivityApnsConfig, VoIPApnsConfig
from .models.apns_priority import ApnsPriority
from .models.apns_push_type import ApnsPushType
from .models.critical_sound import CriticalSound
from .models.interruption_level import InterruptionLevel
from .models.payload import Payload
from .models.payload_alert import PayloadAlert
from .models import (
ApnsConfig,
ApnsPriority,
ApnsPushType,
CriticalSound,
InterruptionLevel,
LiveActivityApnsConfig,
LiveActivityEvent,
LiveActivityPayload,
Payload,
PayloadAlert,
VoIPApnsConfig,
)


__all__ = [
"ApnsClient",
"VoIPClient",
"ApnsConfig",
"LiveActivityApnsConfig",
"VoIPApnsConfig",
"ApnsPriority",
"ApnsPushType",
"CriticalSound",
"InterruptionLevel",
"LiveActivityApnsConfig",
"LiveActivityClient",
"LiveActivityEvent",
"LiveActivityPayload",
"Payload",
"PayloadAlert",
"VoIPClient",
"VoIPApnsConfig",
"exceptions",
]
24 changes: 24 additions & 0 deletions kalyke/clients/live_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from dataclasses import dataclass

from httpx import AsyncClient

from ..models import LiveActivityApnsConfig, LiveActivityPayload
from .apns import ApnsClient


@dataclass
class LiveActivityClient(ApnsClient):
async def send_message(
self,
device_token: str,
payload: LiveActivityPayload,
apns_config: LiveActivityApnsConfig,
) -> str:
return await super().send_message(
device_token=device_token,
payload=payload,
apns_config=apns_config,
)

def _init_client(self, apns_config: LiveActivityApnsConfig) -> AsyncClient:
return super()._init_client(apns_config=apns_config)
14 changes: 13 additions & 1 deletion kalyke/clients/voip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass, field

Check failure on line 1 in kalyke/clients/voip.py

View workflow job for this annotation

GitHub Actions / isort (3.9)

Imports are incorrectly sorted and/or formatted.

Check failure on line 1 in kalyke/clients/voip.py

View workflow job for this annotation

GitHub Actions / isort (3.10)

Imports are incorrectly sorted and/or formatted.

Check failure on line 1 in kalyke/clients/voip.py

View workflow job for this annotation

GitHub Actions / isort (3.11)

Imports are incorrectly sorted and/or formatted.

Check failure on line 1 in kalyke/clients/voip.py

View workflow job for this annotation

GitHub Actions / isort (3.12)

Imports are incorrectly sorted and/or formatted.
from pathlib import Path
from typing import Union
from typing import Union, Dict, Any

import httpx
from httpx import AsyncClient
Expand All @@ -21,6 +21,18 @@ def __post_init__(self):
else:
self._auth_key_filepath = Path(self.auth_key_filepath)

async def send_message(
self,
device_token: str,
payload: Dict[str, Any],
apns_config: VoIPApnsConfig,
) -> str:
return await super().send_message(
device_token=device_token,
payload=payload,
apns_config=apns_config,
)

def _init_client(self, apns_config: VoIPApnsConfig) -> AsyncClient:
headers = apns_config.make_headers()
context = httpx.create_ssl_context()
Expand Down
10 changes: 10 additions & 0 deletions kalyke/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ def __str__(self) -> str:
return f"The system uses the relevance_score, a value between 0 and 1. Did set {self._relevance_score}."


class LiveActivityAttributesIsNotJSONSerializable(Exception):
def __str__(self) -> str:
return "The attributes is not JSON serializable."


class LiveActivityContentStateIsNotJSONSerializable(Exception):
def __str__(self) -> str:
return "The content state is not JSON serializable."


# https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns#3394535
class ApnsProviderException(Exception):
def __init__(self, error: Dict[str, Any]) -> None:
Expand Down
5 changes: 4 additions & 1 deletion kalyke/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from .apns_push_type import ApnsPushType
from .critical_sound import CriticalSound
from .interruption_level import InterruptionLevel
from .payload import Payload
from .live_activity_event import LiveActivityEvent
from .payload import LiveActivityPayload, Payload
from .payload_alert import PayloadAlert

__all__ = [
Expand All @@ -14,6 +15,8 @@
"ApnsPushType",
"CriticalSound",
"InterruptionLevel",
"LiveActivityEvent",
"Payload",
"LiveActivityPayload",
"PayloadAlert",
]
7 changes: 7 additions & 0 deletions kalyke/models/live_activity_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class LiveActivityEvent(Enum):
start: str = "start"
update: str = "update"
end: str = "end"
69 changes: 64 additions & 5 deletions kalyke/models/payload.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, Optional, Union

from ..exceptions import RelevanceScoreOutOfRangeException
from ..exceptions import (
LiveActivityAttributesIsNotJSONSerializable,
LiveActivityContentStateIsNotJSONSerializable,
RelevanceScoreOutOfRangeException,
)
from .critical_sound import CriticalSound
from .interruption_level import InterruptionLevel
from .live_activity_event import LiveActivityEvent
from .payload_alert import PayloadAlert


Expand All @@ -24,10 +31,13 @@ class Payload:

def __post_init__(self):
if self.relevance_score:
if 0.0 <= self.relevance_score <= 1.0:
pass
else:
raise RelevanceScoreOutOfRangeException(relevance_score=self.relevance_score)
self._validate_relevance_score()

def _validate_relevance_score(self):
if 0.0 <= self.relevance_score <= 1.0:
pass
else:
raise RelevanceScoreOutOfRangeException(relevance_score=self.relevance_score)

def dict(self) -> Dict[str, Any]:
aps: Dict[str, Any] = {
Expand All @@ -54,3 +64,52 @@ def dict(self) -> Dict[str, Any]:
payload.update(self.custom)

return payload


@dataclass(frozen=True)
class LiveActivityPayload(Payload):
timestamp: datetime = field(default_factory=datetime.now)
event: LiveActivityEvent = field(default=None)
content_state: Dict[str, Any] = field(default_factory=dict)
stale_date: Optional[datetime] = field(default=None)
dismissal_date: Optional[datetime] = field(default=None)
attributes_type: Optional[str] = field(default=None)
attributes: Optional[Dict[str, Any]] = field(default=None)

def __post_init__(self):
if self.event is None:
raise ValueError("event must be specified.")
elif self.event == LiveActivityEvent.start:
if self.attributes_type is None or self.attributes is None:
raise ValueError(
"""attributes_type and attributes must be specified when event is start.
Please see documentation: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification""" # noqa: E501
)
try:
_ = json.dumps(self.attributes)
except json.decoder.JSONDecodeError:
raise LiveActivityAttributesIsNotJSONSerializable()

try:
_ = json.dumps(self.content_state)
except json.decoder.JSONDecodeError:
raise LiveActivityContentStateIsNotJSONSerializable()
super().__post_init__()

def _validate_relevance_score(self):
# You can set any Double value; for example, 25, 50, 75, or 100.
pass

def dict(self) -> Dict[str, Any]:
payload = super().dict()
additional: Dict[str, Optional[Any]] = {
"timestamp": int(self.timestamp.timestamp()),
"event": self.event.value,
"content-state": self.content_state,
"state-date": int(self.stale_date.timestamp()) if self.stale_date else None,
"dismissal-date": int(self.dismissal_date.timestamp()) if self.dismissal_date else None,
}
additional = {k: v for k, v in additional.items() if v is not None}
payload["aps"].update(additional)

return payload
2 changes: 1 addition & 1 deletion tests/clients/client/test_handle_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def test_handle_error(reason, expect):
client = __Client()
exception = client._handle_error(error_json={"reason": reason})

assert type(exception) == expect
assert isinstance(exception, expect)


def test_attributed_error():
Expand Down

0 comments on commit 02440fb

Please sign in to comment.