Skip to content

Commit 3c4d87f

Browse files
Allow2CEOruvnet
andcommitted
Add model classes
Activity, CheckResult, DayType, OAuthTokens, RequestResult, RequestType (enum), FeedbackCategory (enum), VoiceCodePair -- frozen dataclasses with from_api_response factory methods matching PHP reference. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent ee26d5e commit 3c4d87f

9 files changed

Lines changed: 423 additions & 0 deletions

File tree

allow2_service/models/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Allow2 SDK data models."""
2+
3+
from allow2_service.models.activity import Activity
4+
from allow2_service.models.check_result import CheckResult
5+
from allow2_service.models.day_type import DayType
6+
from allow2_service.models.feedback_category import FeedbackCategory
7+
from allow2_service.models.oauth_tokens import OAuthTokens
8+
from allow2_service.models.request_result import RequestResult
9+
from allow2_service.models.request_type import RequestType
10+
from allow2_service.models.voice_code_pair import VoiceCodePair
11+
12+
__all__ = [
13+
"Activity",
14+
"CheckResult",
15+
"DayType",
16+
"FeedbackCategory",
17+
"OAuthTokens",
18+
"RequestResult",
19+
"RequestType",
20+
"VoiceCodePair",
21+
]

allow2_service/models/activity.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Represents the permission state of a single activity from a check result."""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
from dataclasses import dataclass, field
7+
from typing import Any, ClassVar, Dict
8+
9+
10+
@dataclass(frozen=True)
11+
class Activity:
12+
"""Represents the permission state of a single activity from a check result.
13+
14+
Attributes:
15+
id: The activity ID.
16+
name: Human-readable activity name.
17+
allowed: Whether this activity is currently allowed.
18+
remaining: Remaining seconds for this activity.
19+
banned: Whether this activity is banned indefinitely.
20+
time_block_allowed: Whether the current time block permits this activity.
21+
"""
22+
23+
# Well-known activity IDs (class variables, not dataclass fields)
24+
SCREEN_TIME: ClassVar[int] = 8
25+
GAMING: ClassVar[int] = 3
26+
INTERNET: ClassVar[int] = 1
27+
SOCIAL: ClassVar[int] = 6
28+
29+
id: int
30+
name: str
31+
allowed: bool
32+
remaining: int
33+
banned: bool
34+
time_block_allowed: bool
35+
36+
@staticmethod
37+
def from_array(data: Dict[str, Any]) -> Activity:
38+
"""Create from a single activity entry in the API check response.
39+
40+
Args:
41+
data: Activity dictionary from the API response.
42+
43+
Returns:
44+
An Activity instance.
45+
"""
46+
return Activity(
47+
id=int(data.get("id", 0)),
48+
name=str(data.get("activity", data.get("name", ""))),
49+
allowed=bool(data.get("allowed", False)),
50+
remaining=int(data.get("remaining", 0)),
51+
banned=bool(data.get("banned", False)),
52+
time_block_allowed=bool(
53+
data.get("timeblock", data.get("timeBlockAllowed", True))
54+
),
55+
)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Result of a permission check from the Allow2 service API."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass, field
6+
from typing import Any, Dict, List, Optional
7+
8+
from allow2_service.models.activity import Activity
9+
from allow2_service.models.day_type import DayType
10+
11+
12+
@dataclass(frozen=True)
13+
class CheckResult:
14+
"""Result of a permission check from the Allow2 service API.
15+
16+
Contains the overall allowed status, per-activity breakdown,
17+
and current/upcoming day type information.
18+
19+
Attributes:
20+
allowed: Whether the child is globally allowed right now.
21+
activities: Per-activity permission breakdown.
22+
today_day_type: The current day type.
23+
tomorrow_day_type: The upcoming day type (if provided).
24+
raw: The raw API response for advanced use.
25+
"""
26+
27+
allowed: bool
28+
activities: List[Activity]
29+
today_day_type: Optional[DayType]
30+
tomorrow_day_type: Optional[DayType]
31+
raw: Dict[str, Any] = field(default_factory=dict)
32+
33+
def get_activity(self, activity_id: int) -> Optional[Activity]:
34+
"""Get a specific activity by ID, or None if not present.
35+
36+
Args:
37+
activity_id: The activity ID to look up.
38+
39+
Returns:
40+
The matching Activity, or None.
41+
"""
42+
for activity in self.activities:
43+
if activity.id == activity_id:
44+
return activity
45+
return None
46+
47+
def is_activity_allowed(self, activity_id: int) -> bool:
48+
"""Check whether a specific activity is allowed.
49+
50+
Args:
51+
activity_id: The activity ID to check.
52+
53+
Returns:
54+
True if the activity is present and allowed.
55+
"""
56+
activity = self.get_activity(activity_id)
57+
return activity is not None and activity.allowed
58+
59+
def get_remaining_seconds(self, activity_id: int) -> int:
60+
"""Get remaining seconds for a specific activity.
61+
62+
Args:
63+
activity_id: The activity ID to check.
64+
65+
Returns:
66+
Remaining seconds, or 0 if activity not found.
67+
"""
68+
activity = self.get_activity(activity_id)
69+
return activity.remaining if activity is not None else 0
70+
71+
@staticmethod
72+
def from_api_response(response: Dict[str, Any]) -> CheckResult:
73+
"""Build from the raw API response.
74+
75+
Args:
76+
response: The decoded JSON response from /serviceapi/check.
77+
78+
Returns:
79+
A CheckResult instance.
80+
"""
81+
activities: List[Activity] = []
82+
raw_activities = response.get("activities", [])
83+
84+
for act_data in raw_activities:
85+
activities.append(Activity.from_array(act_data))
86+
87+
today_day_type: Optional[DayType] = None
88+
if "dayType" in response:
89+
today_day_type = DayType.from_array(response["dayType"])
90+
elif "today" in response:
91+
today_day_type = DayType.from_array(response["today"])
92+
93+
tomorrow_day_type: Optional[DayType] = None
94+
if "tomorrowDayType" in response:
95+
tomorrow_day_type = DayType.from_array(response["tomorrowDayType"])
96+
elif "tomorrow" in response:
97+
tomorrow_day_type = DayType.from_array(response["tomorrow"])
98+
99+
# Global "allowed" is true only if ALL activities are allowed
100+
allowed = True
101+
for act in activities:
102+
if not act.allowed:
103+
allowed = False
104+
break
105+
106+
# Override with explicit server value if present
107+
if "allowed" in response:
108+
allowed = bool(response["allowed"])
109+
110+
return CheckResult(
111+
allowed=allowed,
112+
activities=activities,
113+
today_day_type=today_day_type,
114+
tomorrow_day_type=tomorrow_day_type,
115+
raw=response,
116+
)

allow2_service/models/day_type.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Represents a day type in the Allow2 system (e.g., School Day, Weekend, Holiday)."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import Any, Dict
7+
8+
9+
@dataclass(frozen=True)
10+
class DayType:
11+
"""Represents a day type in the Allow2 system.
12+
13+
Attributes:
14+
id: The day type ID.
15+
name: Human-readable name (e.g., "School Day", "Weekend").
16+
"""
17+
18+
id: int
19+
name: str
20+
21+
@staticmethod
22+
def from_array(data: Dict[str, Any]) -> DayType:
23+
"""Create from API response data.
24+
25+
Args:
26+
data: Day type dictionary from the API response.
27+
28+
Returns:
29+
A DayType instance.
30+
"""
31+
return DayType(
32+
id=int(data["id"]),
33+
name=str(data["name"]),
34+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Categories for feedback submissions."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum
6+
7+
8+
class FeedbackCategory(Enum):
9+
"""Categories for feedback submissions."""
10+
11+
BUG = "bug"
12+
FEATURE_REQUEST = "feature_request"
13+
NOT_WORKING = "not_working"
14+
OTHER = "other"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Value object representing an OAuth2 token set."""
2+
3+
from __future__ import annotations
4+
5+
import time
6+
from dataclasses import dataclass
7+
from typing import Any, Dict
8+
9+
10+
@dataclass(frozen=True)
11+
class OAuthTokens:
12+
"""Value object representing an OAuth2 token set.
13+
14+
Attributes:
15+
access_token: The OAuth2 access token.
16+
refresh_token: The OAuth2 refresh token.
17+
expires_at: Unix timestamp when the access token expires.
18+
"""
19+
20+
access_token: str
21+
refresh_token: str
22+
expires_at: int
23+
24+
def is_expired(self, buffer_seconds: int = 300) -> bool:
25+
"""Check whether the access token has expired or is about to expire.
26+
27+
Args:
28+
buffer_seconds: Seconds before actual expiry to consider "expired"
29+
(default 300 = 5 min).
30+
31+
Returns:
32+
True if the token should be refreshed.
33+
"""
34+
return int(time.time()) >= (self.expires_at - buffer_seconds)
35+
36+
@staticmethod
37+
def from_api_response(response: Dict[str, Any]) -> OAuthTokens:
38+
"""Create from an API token response.
39+
40+
Args:
41+
response: Dictionary with ``access_token``, ``refresh_token``,
42+
and ``expires_in`` keys.
43+
44+
Returns:
45+
An OAuthTokens instance.
46+
"""
47+
return OAuthTokens(
48+
access_token=response["access_token"],
49+
refresh_token=response["refresh_token"],
50+
expires_at=int(time.time()) + int(response["expires_in"]),
51+
)
52+
53+
def to_dict(self) -> Dict[str, Any]:
54+
"""Serialize to dictionary for storage.
55+
56+
Returns:
57+
Dictionary with ``access_token``, ``refresh_token``,
58+
and ``expires_at`` keys.
59+
"""
60+
return {
61+
"access_token": self.access_token,
62+
"refresh_token": self.refresh_token,
63+
"expires_at": self.expires_at,
64+
}
65+
66+
@staticmethod
67+
def from_dict(data: Dict[str, Any]) -> OAuthTokens:
68+
"""Reconstitute from stored dictionary.
69+
70+
Args:
71+
data: Dictionary with ``access_token``, ``refresh_token``,
72+
and ``expires_at`` keys.
73+
74+
Returns:
75+
An OAuthTokens instance.
76+
"""
77+
return OAuthTokens(
78+
access_token=data["access_token"],
79+
refresh_token=data["refresh_token"],
80+
expires_at=int(data["expires_at"]),
81+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Result of creating a request (more time, day type change, or ban lift)."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import Any, Dict
7+
8+
9+
@dataclass(frozen=True)
10+
class RequestResult:
11+
"""Result of creating a request (more time, day type change, or ban lift).
12+
13+
Attributes:
14+
request_id: The unique request identifier.
15+
status_secret: Secret for polling the request status.
16+
status: Current status string ("pending", "approved", or "denied").
17+
"""
18+
19+
request_id: str
20+
status_secret: str
21+
status: str
22+
23+
@staticmethod
24+
def from_api_response(response: Dict[str, Any]) -> RequestResult:
25+
"""Create from API response.
26+
27+
Args:
28+
response: Dictionary with ``requestId``, ``statusSecret``,
29+
and optionally ``status`` keys.
30+
31+
Returns:
32+
A RequestResult instance.
33+
"""
34+
return RequestResult(
35+
request_id=str(response["requestId"]),
36+
status_secret=str(response["statusSecret"]),
37+
status=str(response.get("status", "pending")),
38+
)
39+
40+
def is_pending(self) -> bool:
41+
"""Whether the request is still pending."""
42+
return self.status == "pending"
43+
44+
def is_approved(self) -> bool:
45+
"""Whether the request was approved."""
46+
return self.status == "approved"
47+
48+
def is_denied(self) -> bool:
49+
"""Whether the request was denied."""
50+
return self.status == "denied"

0 commit comments

Comments
 (0)