pynintendoparental 2.0.0__tar.gz → 2.1.1__tar.gz
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.
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/PKG-INFO +7 -3
- pynintendoparental-2.1.1/pynintendoparental/_version.py +1 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/api.py +0 -14
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/const.py +2 -2
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/device.py +29 -7
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/exceptions.py +2 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/player.py +17 -8
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/PKG-INFO +7 -3
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/SOURCES.txt +5 -1
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/requires.txt +6 -2
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/setup.py +6 -2
- pynintendoparental-2.1.1/tests/test_api.py +193 -0
- pynintendoparental-2.1.1/tests/test_device.py +110 -0
- pynintendoparental-2.1.1/tests/test_init.py +80 -0
- pynintendoparental-2.1.1/tests/test_player.py +46 -0
- pynintendoparental-2.0.0/pynintendoparental/_version.py +0 -1
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/LICENSE +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/README.md +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/__init__.py +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/application.py +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/authenticator.py +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/enum.py +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/py.typed +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental/utils.py +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/dependency_links.txt +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/top_level.txt +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pyproject.toml +0 -0
- {pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pynintendoparental
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: A Python module to interact with Nintendo Parental Controls
|
|
5
5
|
Home-page: http://github.com/pantherale0/pynintendoparental
|
|
6
6
|
Author: pantherale0
|
|
@@ -11,16 +11,20 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.8, <4
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: pynintendoauth
|
|
14
|
+
Requires-Dist: pynintendoauth==1.0.0
|
|
15
15
|
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: aiofiles<26,>=23; extra == "dev"
|
|
16
17
|
Requires-Dist: bandit<1.9,>=1.7; extra == "dev"
|
|
17
18
|
Requires-Dist: black<26,>=23; extra == "dev"
|
|
18
19
|
Requires-Dist: build<1.4,>=0.10; extra == "dev"
|
|
20
|
+
Requires-Dist: Faker<39,>=38; extra == "dev"
|
|
19
21
|
Requires-Dist: flake8<8,>=6; extra == "dev"
|
|
20
22
|
Requires-Dist: isort<7,>=5; extra == "dev"
|
|
21
|
-
Requires-Dist: mypy<1.
|
|
23
|
+
Requires-Dist: mypy<1.20,>=1.5; extra == "dev"
|
|
22
24
|
Requires-Dist: pytest<9,>=7; extra == "dev"
|
|
23
25
|
Requires-Dist: pytest-cov<8,>=4; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.21; extra == "dev"
|
|
27
|
+
Requires-Dist: syrupy<6,>=5; extra == "dev"
|
|
24
28
|
Requires-Dist: twine<7,>=4; extra == "dev"
|
|
25
29
|
Dynamic: author
|
|
26
30
|
Dynamic: classifier
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.1"
|
|
@@ -112,12 +112,6 @@ class Api:
|
|
|
112
112
|
# now return the resp dict
|
|
113
113
|
return resp
|
|
114
114
|
|
|
115
|
-
async def async_get_account_details(self) -> dict:
|
|
116
|
-
"""Get account details."""
|
|
117
|
-
return await self.send_request(
|
|
118
|
-
endpoint="get_account_details", ACCOUNT_ID=self.account_id
|
|
119
|
-
)
|
|
120
|
-
|
|
121
115
|
async def async_get_account_devices(self) -> dict:
|
|
122
116
|
"""Get account devices."""
|
|
123
117
|
return await self.send_request(endpoint="get_account_devices")
|
|
@@ -146,14 +140,6 @@ class Api:
|
|
|
146
140
|
endpoint="get_device_parental_control_setting", DEVICE_ID=device_id
|
|
147
141
|
)
|
|
148
142
|
|
|
149
|
-
async def async_get_device_parental_control_setting_state(
|
|
150
|
-
self, device_id: str
|
|
151
|
-
) -> dict:
|
|
152
|
-
"""Get device parental control setting state."""
|
|
153
|
-
return await self.send_request(
|
|
154
|
-
endpoint="get_device_parental_control_setting_state", DEVICE_ID=device_id
|
|
155
|
-
)
|
|
156
|
-
|
|
157
143
|
async def async_get_device_monthly_summary(
|
|
158
144
|
self, device_id: str, year: int, month: int
|
|
159
145
|
) -> dict:
|
|
@@ -6,8 +6,8 @@ import logging
|
|
|
6
6
|
_LOGGER = logging.getLogger(__package__)
|
|
7
7
|
CLIENT_ID = "54789befb391a838"
|
|
8
8
|
MOBILE_APP_PKG = "com.nintendo.znma"
|
|
9
|
-
MOBILE_APP_VERSION = "2.
|
|
10
|
-
MOBILE_APP_BUILD = "
|
|
9
|
+
MOBILE_APP_VERSION = "2.3.0"
|
|
10
|
+
MOBILE_APP_BUILD = "600"
|
|
11
11
|
OS_NAME = "ANDROID"
|
|
12
12
|
OS_VERSION = "34"
|
|
13
13
|
OS_STR = f"{OS_NAME} {OS_VERSION}"
|
|
@@ -32,8 +32,9 @@ class Device:
|
|
|
32
32
|
self._api: Api = api
|
|
33
33
|
self.daily_summaries: dict = {}
|
|
34
34
|
self.parental_control_settings: dict = {}
|
|
35
|
-
self.players:
|
|
35
|
+
self.players: dict[str, Player] = {}
|
|
36
36
|
self.limit_time: int | float | None = 0
|
|
37
|
+
self.extra_playing_time: int | None = None
|
|
37
38
|
self.timer_mode: DeviceTimerMode | None = None
|
|
38
39
|
self.today_playing_time: int | float = 0
|
|
39
40
|
self.today_time_remaining: int | float = 0
|
|
@@ -66,6 +67,13 @@ class Device:
|
|
|
66
67
|
"""Return the generation."""
|
|
67
68
|
return self.extra.get("platformGeneration", None)
|
|
68
69
|
|
|
70
|
+
@property
|
|
71
|
+
def last_sync(self) -> float | None:
|
|
72
|
+
"""Return the last time this device was synced."""
|
|
73
|
+
return self.extra.get("synchronizedParentalControlSetting", {}).get(
|
|
74
|
+
"synchronizedAt", None
|
|
75
|
+
)
|
|
76
|
+
|
|
69
77
|
async def update(self):
|
|
70
78
|
"""Update data."""
|
|
71
79
|
_LOGGER.debug(">> Device.update()")
|
|
@@ -76,11 +84,8 @@ class Device:
|
|
|
76
84
|
self.get_monthly_summary(),
|
|
77
85
|
self._get_extras(),
|
|
78
86
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else:
|
|
82
|
-
for player in self.players:
|
|
83
|
-
player.update_from_daily_summary(self.daily_summaries)
|
|
87
|
+
for player in self.players.values():
|
|
88
|
+
player.update_from_daily_summary(self.daily_summaries)
|
|
84
89
|
await self._execute_callbacks()
|
|
85
90
|
|
|
86
91
|
def add_device_callback(self, callback):
|
|
@@ -321,6 +326,14 @@ class Device:
|
|
|
321
326
|
today_reg = self._get_today_regulation(now)
|
|
322
327
|
limit_time = today_reg.get("timeToPlayInOneDay", {}).get("limitTime")
|
|
323
328
|
self.limit_time = limit_time if limit_time is not None else -1
|
|
329
|
+
extra_playing_time_data = (
|
|
330
|
+
pcs.get("ownedDevice", {}).get("device", {}).get("extraPlayingTime")
|
|
331
|
+
)
|
|
332
|
+
self.extra_playing_time = None
|
|
333
|
+
if extra_playing_time_data:
|
|
334
|
+
self.extra_playing_time = extra_playing_time_data.get("inOneDay", {}).get(
|
|
335
|
+
"duration"
|
|
336
|
+
)
|
|
324
337
|
|
|
325
338
|
bedtime_setting = today_reg.get("bedtime", {})
|
|
326
339
|
if bedtime_setting.get("enabled"):
|
|
@@ -527,6 +540,15 @@ class Device:
|
|
|
527
540
|
)
|
|
528
541
|
if latest:
|
|
529
542
|
self.last_month_summary = summary = response["json"]["summary"]
|
|
543
|
+
# Generate player objects
|
|
544
|
+
for player in response.get("json", {}).get("summary", {}).get("players", []):
|
|
545
|
+
profile = player.get("profile")
|
|
546
|
+
if not profile or not profile.get("playerId"):
|
|
547
|
+
continue
|
|
548
|
+
player_id = profile["playerId"]
|
|
549
|
+
if player_id not in self.players:
|
|
550
|
+
self.players[player_id] = Player.from_profile(profile)
|
|
551
|
+
self.players[player_id].month_summary = player.get("summary", {})
|
|
530
552
|
return summary
|
|
531
553
|
return response["json"]["summary"]
|
|
532
554
|
|
|
@@ -564,7 +586,7 @@ class Device:
|
|
|
564
586
|
|
|
565
587
|
def get_player(self, player_id: str) -> Player:
|
|
566
588
|
"""Returns a player."""
|
|
567
|
-
player =
|
|
589
|
+
player = self.players.get(player_id)
|
|
568
590
|
if player:
|
|
569
591
|
return player
|
|
570
592
|
raise ValueError(f"Player with id {player_id} not found.")
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from enum import StrEnum
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class RangeErrorKeys(StrEnum):
|
|
6
7
|
"""Keys for range errors."""
|
|
7
8
|
|
|
8
9
|
DAILY_PLAYTIME = "daily_playtime_out_of_range"
|
|
9
10
|
BEDTIME = "bedtime_alarm_out_of_range"
|
|
10
11
|
|
|
12
|
+
|
|
11
13
|
class NoDevicesFoundException(Exception):
|
|
12
14
|
"""No devices were found for the account."""
|
|
13
15
|
|
|
@@ -8,22 +8,22 @@ class Player:
|
|
|
8
8
|
|
|
9
9
|
def __init__(self):
|
|
10
10
|
"""Init a player."""
|
|
11
|
-
self.player_image: str = None
|
|
12
|
-
self.nickname: str = None
|
|
11
|
+
self.player_image: str | None = None
|
|
12
|
+
self.nickname: str | None = None
|
|
13
13
|
self.apps: list = []
|
|
14
|
-
self.
|
|
15
|
-
self.
|
|
14
|
+
self.month_summary: dict = {}
|
|
15
|
+
self.player_id: str | None = None
|
|
16
|
+
self.playing_time: int = 0
|
|
16
17
|
|
|
17
18
|
def update_from_daily_summary(self, raw: list[dict]):
|
|
18
19
|
"""Update the current instance of the player from the daily summery"""
|
|
19
20
|
_LOGGER.debug("Updating player %s daily summary", self.player_id)
|
|
20
|
-
for player in raw[0].get("
|
|
21
|
-
if self.player_id
|
|
22
|
-
self.player_id = player["profile"].get("playerId")
|
|
21
|
+
for player in raw[0].get("players", []):
|
|
22
|
+
if self.player_id == player["profile"].get("playerId"):
|
|
23
23
|
self.player_image = player["profile"].get("imageUri")
|
|
24
24
|
self.nickname = player["profile"].get("nickname")
|
|
25
25
|
self.playing_time = player.get("playingTime")
|
|
26
|
-
self.apps = player.get("
|
|
26
|
+
self.apps = player.get("playedGames")
|
|
27
27
|
break
|
|
28
28
|
|
|
29
29
|
@classmethod
|
|
@@ -41,3 +41,12 @@ class Player:
|
|
|
41
41
|
players.append(parsed)
|
|
42
42
|
_LOGGER.debug("Built player %s", parsed.player_id)
|
|
43
43
|
return players
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_profile(cls, raw: dict) -> "Player":
|
|
47
|
+
"""Converts a profile response into a player."""
|
|
48
|
+
parsed = cls()
|
|
49
|
+
parsed.player_id = raw.get("playerId")
|
|
50
|
+
parsed.player_image = raw.get("imageUri")
|
|
51
|
+
parsed.nickname = raw.get("nickname")
|
|
52
|
+
return parsed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pynintendoparental
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: A Python module to interact with Nintendo Parental Controls
|
|
5
5
|
Home-page: http://github.com/pantherale0/pynintendoparental
|
|
6
6
|
Author: pantherale0
|
|
@@ -11,16 +11,20 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.8, <4
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: pynintendoauth
|
|
14
|
+
Requires-Dist: pynintendoauth==1.0.0
|
|
15
15
|
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: aiofiles<26,>=23; extra == "dev"
|
|
16
17
|
Requires-Dist: bandit<1.9,>=1.7; extra == "dev"
|
|
17
18
|
Requires-Dist: black<26,>=23; extra == "dev"
|
|
18
19
|
Requires-Dist: build<1.4,>=0.10; extra == "dev"
|
|
20
|
+
Requires-Dist: Faker<39,>=38; extra == "dev"
|
|
19
21
|
Requires-Dist: flake8<8,>=6; extra == "dev"
|
|
20
22
|
Requires-Dist: isort<7,>=5; extra == "dev"
|
|
21
|
-
Requires-Dist: mypy<1.
|
|
23
|
+
Requires-Dist: mypy<1.20,>=1.5; extra == "dev"
|
|
22
24
|
Requires-Dist: pytest<9,>=7; extra == "dev"
|
|
23
25
|
Requires-Dist: pytest-cov<8,>=4; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.21; extra == "dev"
|
|
27
|
+
Requires-Dist: syrupy<6,>=5; extra == "dev"
|
|
24
28
|
Requires-Dist: twine<7,>=4; extra == "dev"
|
|
25
29
|
Dynamic: author
|
|
26
30
|
Dynamic: classifier
|
{pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/SOURCES.txt
RENAMED
|
@@ -18,4 +18,8 @@ pynintendoparental.egg-info/PKG-INFO
|
|
|
18
18
|
pynintendoparental.egg-info/SOURCES.txt
|
|
19
19
|
pynintendoparental.egg-info/dependency_links.txt
|
|
20
20
|
pynintendoparental.egg-info/requires.txt
|
|
21
|
-
pynintendoparental.egg-info/top_level.txt
|
|
21
|
+
pynintendoparental.egg-info/top_level.txt
|
|
22
|
+
tests/test_api.py
|
|
23
|
+
tests/test_device.py
|
|
24
|
+
tests/test_init.py
|
|
25
|
+
tests/test_player.py
|
{pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/requires.txt
RENAMED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
pynintendoauth
|
|
1
|
+
pynintendoauth==1.0.0
|
|
2
2
|
|
|
3
3
|
[dev]
|
|
4
|
+
aiofiles<26,>=23
|
|
4
5
|
bandit<1.9,>=1.7
|
|
5
6
|
black<26,>=23
|
|
6
7
|
build<1.4,>=0.10
|
|
8
|
+
Faker<39,>=38
|
|
7
9
|
flake8<8,>=6
|
|
8
10
|
isort<7,>=5
|
|
9
|
-
mypy<1.
|
|
11
|
+
mypy<1.20,>=1.5
|
|
10
12
|
pytest<9,>=7
|
|
11
13
|
pytest-cov<8,>=4
|
|
14
|
+
pytest-asyncio<1.0,>=0.21
|
|
15
|
+
syrupy<6,>=5
|
|
12
16
|
twine<7,>=4
|
|
@@ -16,18 +16,22 @@ with open('pynintendoparental/_version.py', 'r') as version_file:
|
|
|
16
16
|
REQUIREMENTS = [
|
|
17
17
|
# Add your list of production dependencies here, eg:
|
|
18
18
|
# 'requests == 2.*',
|
|
19
|
-
'pynintendoauth
|
|
19
|
+
'pynintendoauth==1.0.0'
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
DEV_REQUIREMENTS = [
|
|
23
|
+
'aiofiles >= 23,< 26',
|
|
23
24
|
'bandit >= 1.7,< 1.9',
|
|
24
25
|
'black >= 23,< 26',
|
|
25
26
|
'build >= 0.10,< 1.4',
|
|
27
|
+
'Faker >= 38,< 39',
|
|
26
28
|
'flake8 >= 6,< 8',
|
|
27
29
|
'isort >= 5,< 7',
|
|
28
|
-
'mypy >= 1.5,< 1.
|
|
30
|
+
'mypy >= 1.5,< 1.20',
|
|
29
31
|
'pytest >= 7,< 9',
|
|
30
32
|
'pytest-cov >= 4,< 8',
|
|
33
|
+
'pytest-asyncio >= 0.21,< 1.0',
|
|
34
|
+
'syrupy >= 5,< 6',
|
|
31
35
|
'twine >= 4,< 7',
|
|
32
36
|
]
|
|
33
37
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Tests for the API class."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from aiohttp import ContentTypeError
|
|
7
|
+
from pynintendoauth.exceptions import HttpException
|
|
8
|
+
|
|
9
|
+
from pynintendoparental.api import Api, _check_http_success
|
|
10
|
+
from pynintendoparental.authenticator import Authenticator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize(
|
|
14
|
+
"status, expected", [(200, True), (204, True), (300, False), (404, False)]
|
|
15
|
+
)
|
|
16
|
+
def test_check_http_success(status, expected):
|
|
17
|
+
"""Test the _check_http_success function."""
|
|
18
|
+
assert _check_http_success(status) == expected
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_api_init_and_properties(mock_authenticator: Authenticator):
|
|
22
|
+
"""Test API initialization and properties."""
|
|
23
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
24
|
+
assert api.account_id == "ACCOUNT_ID"
|
|
25
|
+
headers = api._headers
|
|
26
|
+
assert headers["Authorization"] == "ACCESS_TOKEN"
|
|
27
|
+
assert headers["X-Moon-TimeZone"] == "Europe/London"
|
|
28
|
+
assert headers["X-Moon-App-Language"] == "en-GB"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def test_send_request_invalid_endpoint(mock_authenticator: Authenticator):
|
|
32
|
+
"""Test sending a request to an invalid endpoint."""
|
|
33
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
34
|
+
with pytest.raises(ValueError, match="Endpoint does not exist"):
|
|
35
|
+
await api.send_request("invalid_endpoint")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def test_send_request_token_refresh(mock_authenticator: Authenticator):
|
|
39
|
+
"""Test that the token is refreshed if it's expired."""
|
|
40
|
+
mock_authenticator.access_token_expired = True
|
|
41
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
42
|
+
|
|
43
|
+
mock_response = MagicMock()
|
|
44
|
+
mock_response.status = 200
|
|
45
|
+
mock_response.json = AsyncMock(return_value={"status": "ok"})
|
|
46
|
+
mock_response.text = AsyncMock(return_value='{"status": "ok"}')
|
|
47
|
+
mock_authenticator.async_authenticated_request = AsyncMock(
|
|
48
|
+
return_value=mock_response
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await api.send_request("get_account_devices")
|
|
52
|
+
mock_authenticator.perform_refresh.assert_called_once()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def test_send_request_http_exception(mock_authenticator: Authenticator):
|
|
56
|
+
"""Test a generic HttpException is raised on non-2xx response."""
|
|
57
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
58
|
+
|
|
59
|
+
mock_response = MagicMock()
|
|
60
|
+
mock_response.status = 500
|
|
61
|
+
mock_response.content_type = "text/plain"
|
|
62
|
+
mock_response.text = AsyncMock(return_value="Internal Server Error")
|
|
63
|
+
mock_authenticator.async_authenticated_request = AsyncMock(
|
|
64
|
+
return_value=mock_response
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
with pytest.raises(HttpException, match="Internal Server Error"):
|
|
68
|
+
await api.send_request("get_account_devices")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def test_send_request_http_exception_problem_json(
|
|
72
|
+
mock_authenticator: Authenticator,
|
|
73
|
+
):
|
|
74
|
+
"""Test that HttpException is raised with details from a problem+json response."""
|
|
75
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
76
|
+
|
|
77
|
+
mock_response = MagicMock()
|
|
78
|
+
mock_response.status = 400
|
|
79
|
+
mock_response.content_type = "application/problem+json"
|
|
80
|
+
mock_response.json = AsyncMock(
|
|
81
|
+
return_value={"detail": "Bad Request", "errorCode": "E0001"}
|
|
82
|
+
)
|
|
83
|
+
mock_authenticator.async_authenticated_request = AsyncMock(
|
|
84
|
+
return_value=mock_response
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
with pytest.raises(HttpException, match="Bad Request"):
|
|
88
|
+
await api.send_request("get_account_devices")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def test_send_request_http_exception_problem_json_invalid(
|
|
92
|
+
mock_authenticator: Authenticator,
|
|
93
|
+
):
|
|
94
|
+
"""Test HttpException with a generic message for invalid problem+json."""
|
|
95
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
96
|
+
mock_request_info = Mock(real_url="http://mock.url")
|
|
97
|
+
content_type_error = ContentTypeError(mock_request_info, ())
|
|
98
|
+
|
|
99
|
+
mock_response = MagicMock()
|
|
100
|
+
mock_response.status = 400
|
|
101
|
+
mock_response.content_type = "application/problem+json"
|
|
102
|
+
mock_response.json = AsyncMock(side_effect=content_type_error)
|
|
103
|
+
mock_response.text = AsyncMock(return_value="Invalid JSON")
|
|
104
|
+
mock_authenticator.async_authenticated_request = AsyncMock(
|
|
105
|
+
return_value=mock_response
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
with pytest.raises(HttpException, match="Invalid JSON"):
|
|
109
|
+
await api.send_request("get_account_devices")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def test_send_request_json_decode_error(mock_authenticator: Authenticator):
|
|
113
|
+
"""Test that an empty json dict is returned on a JSON decode error."""
|
|
114
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
115
|
+
mock_request_info = Mock(real_url="http://mock.url")
|
|
116
|
+
content_type_error = ContentTypeError(mock_request_info, ())
|
|
117
|
+
|
|
118
|
+
mock_response = MagicMock()
|
|
119
|
+
mock_response.status = 200
|
|
120
|
+
mock_response.json = AsyncMock(side_effect=content_type_error)
|
|
121
|
+
mock_response.text = AsyncMock(return_value="<not_json>")
|
|
122
|
+
mock_response.url = "http://mock.url"
|
|
123
|
+
mock_authenticator.async_authenticated_request = AsyncMock(
|
|
124
|
+
return_value=mock_response
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
result = await api.send_request("get_account_devices")
|
|
128
|
+
assert result["json"] == {}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def test_api_methods(mock_authenticator: Authenticator):
|
|
132
|
+
"""Test that API methods call send_request with correct parameters."""
|
|
133
|
+
api = Api(auth=mock_authenticator, tz="Europe/London", lang="en-GB")
|
|
134
|
+
api.send_request = AsyncMock()
|
|
135
|
+
|
|
136
|
+
await api.async_get_account_devices()
|
|
137
|
+
api.send_request.assert_called_with(endpoint="get_account_devices")
|
|
138
|
+
|
|
139
|
+
await api.async_get_account_device("DEVICE_ID")
|
|
140
|
+
api.send_request.assert_called_with(
|
|
141
|
+
endpoint="get_account_device", DEVICE_ID="DEVICE_ID"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
await api.async_get_device_daily_summaries("DEVICE_ID")
|
|
145
|
+
api.send_request.assert_called_with(
|
|
146
|
+
endpoint="get_device_daily_summaries", DEVICE_ID="DEVICE_ID"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
await api.async_get_device_monthly_summaries("DEVICE_ID")
|
|
150
|
+
api.send_request.assert_called_with(
|
|
151
|
+
endpoint="get_device_monthly_summaries", DEVICE_ID="DEVICE_ID"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
await api.async_get_device_parental_control_setting("DEVICE_ID")
|
|
155
|
+
api.send_request.assert_called_with(
|
|
156
|
+
endpoint="get_device_parental_control_setting", DEVICE_ID="DEVICE_ID"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
await api.async_get_device_monthly_summary("DEVICE_ID", 2023, 11)
|
|
160
|
+
api.send_request.assert_called_with(
|
|
161
|
+
endpoint="get_device_monthly_summary",
|
|
162
|
+
DEVICE_ID="DEVICE_ID",
|
|
163
|
+
YEAR=2023,
|
|
164
|
+
MONTH="11",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
await api.async_update_restriction_level({"some": "setting"})
|
|
168
|
+
api.send_request.assert_called_with(
|
|
169
|
+
endpoint="update_restriction_level", body={"some": "setting"}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
await api.async_update_extra_playing_time("DEVICE_ID", -1)
|
|
173
|
+
api.send_request.assert_called_with(
|
|
174
|
+
endpoint="update_extra_playing_time",
|
|
175
|
+
body={"deviceId": "DEVICE_ID", "status": "TO_INFINITY"},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
await api.async_update_extra_playing_time("DEVICE_ID", 60)
|
|
179
|
+
api.send_request.assert_called_with(
|
|
180
|
+
endpoint="update_extra_playing_time",
|
|
181
|
+
body={"deviceId": "DEVICE_ID", "additionalTime": 60, "status": "TO_ADDED"},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
await api.async_update_play_timer({"some": "setting"})
|
|
185
|
+
api.send_request.assert_called_with(
|
|
186
|
+
endpoint="update_play_timer", body={"some": "setting"}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
await api.async_update_unlock_code("1234", "DEVICE_ID")
|
|
190
|
+
api.send_request.assert_called_with(
|
|
191
|
+
endpoint="update_unlock_code",
|
|
192
|
+
body={"deviceId": "DEVICE_ID", "unlockCode": "1234"},
|
|
193
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Tests for the Device class."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from syrupy.assertion import SnapshotAssertion
|
|
7
|
+
from syrupy.filters import props
|
|
8
|
+
|
|
9
|
+
from pynintendoauth.exceptions import HttpException
|
|
10
|
+
from pynintendoparental.device import Device
|
|
11
|
+
from pynintendoparental.api import Api
|
|
12
|
+
|
|
13
|
+
from .helpers import load_fixture, clean_device_for_snapshot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def test_device_parsing(mock_api: Api, snapshot: SnapshotAssertion):
|
|
17
|
+
"""Test that the device class parsing works as expected."""
|
|
18
|
+
devices_response = await load_fixture("account_devices")
|
|
19
|
+
devices = await Device.from_devices_response(devices_response, mock_api)
|
|
20
|
+
assert len(devices) > 0
|
|
21
|
+
device = devices[0]
|
|
22
|
+
|
|
23
|
+
mock_api.async_get_device_monthly_summary.assert_called_once()
|
|
24
|
+
|
|
25
|
+
test_device = copy.deepcopy(device)
|
|
26
|
+
assert test_device.last_sync is not None
|
|
27
|
+
assert test_device.last_sync == device.extra["synchronizedParentalControlSetting"][
|
|
28
|
+
"synchronizedAt"
|
|
29
|
+
]
|
|
30
|
+
del test_device.extra["synchronizedParentalControlSetting"]["synchronizedAt"]
|
|
31
|
+
assert test_device.last_sync is None
|
|
32
|
+
del test_device.extra["synchronizedParentalControlSetting"]
|
|
33
|
+
assert test_device.last_sync is None
|
|
34
|
+
|
|
35
|
+
assert clean_device_for_snapshot(device) == snapshot(
|
|
36
|
+
exclude=props("today_time_remaining")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def test_player_discovery(mock_api: Api):
|
|
40
|
+
"""Test that the device correctly parses players in different scenarios."""
|
|
41
|
+
devices_response = await load_fixture("account_devices")
|
|
42
|
+
devices = await Device.from_devices_response(devices_response, mock_api)
|
|
43
|
+
assert len(devices) > 0
|
|
44
|
+
device = devices[0]
|
|
45
|
+
assert len(device.players) > 0
|
|
46
|
+
|
|
47
|
+
# Test that the library correctly handles cases where the playerId is not found
|
|
48
|
+
monthly_summary_response = await load_fixture("device_monthly_summary")
|
|
49
|
+
players = copy.deepcopy(monthly_summary_response["summary"]["players"][0])
|
|
50
|
+
del players["profile"]["playerId"]
|
|
51
|
+
monthly_summary_response["summary"]["players"][0] = players
|
|
52
|
+
|
|
53
|
+
mock_api.async_get_device_monthly_summary.return_value = {
|
|
54
|
+
"status": 200,
|
|
55
|
+
"json": monthly_summary_response,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await device.update()
|
|
59
|
+
assert mock_api.async_get_device_monthly_summary.call_count == 2
|
|
60
|
+
|
|
61
|
+
async def test_get_player(mock_api: Api):
|
|
62
|
+
"""Test that the get_player method works as expected."""
|
|
63
|
+
devices_response = await load_fixture("account_devices")
|
|
64
|
+
devices = await Device.from_devices_response(devices_response, mock_api)
|
|
65
|
+
assert len(devices) > 0
|
|
66
|
+
device = devices[0]
|
|
67
|
+
assert len(device.players) > 0
|
|
68
|
+
|
|
69
|
+
# Get the ID of the first player in the dictionary
|
|
70
|
+
first_player_id = list(device.players.keys())[0]
|
|
71
|
+
player = device.get_player(first_player_id)
|
|
72
|
+
assert player.player_id == first_player_id
|
|
73
|
+
|
|
74
|
+
# Now test that it errors
|
|
75
|
+
with pytest.raises(ValueError):
|
|
76
|
+
device.get_player("invalid_player_id")
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize(
|
|
79
|
+
"mock_api_function,side_effect,expected_log",
|
|
80
|
+
[
|
|
81
|
+
pytest.param(
|
|
82
|
+
"async_get_device_monthly_summary",
|
|
83
|
+
HttpException(404, "test", "test"),
|
|
84
|
+
"HTTP Exception raised while getting monthly summary for device {DEVICE_ID}: HTTP 404: test (test)"
|
|
85
|
+
),
|
|
86
|
+
pytest.param(
|
|
87
|
+
"async_get_device_monthly_summaries",
|
|
88
|
+
HttpException(404, "test", "test"),
|
|
89
|
+
"Could not retrieve monthly summaries: HTTP 404: test (test)"
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
async def test_get_monthly_summary_error(
|
|
94
|
+
mock_api: Api,
|
|
95
|
+
caplog: pytest.LogCaptureFixture,
|
|
96
|
+
mock_api_function: str,
|
|
97
|
+
side_effect: Exception,
|
|
98
|
+
expected_log: str
|
|
99
|
+
):
|
|
100
|
+
"""Test that get_monthly_summary calls correctly handle and log HTTP exceptions."""
|
|
101
|
+
devices_response = await load_fixture("account_devices")
|
|
102
|
+
devices = await Device.from_devices_response(devices_response, mock_api)
|
|
103
|
+
assert len(devices) > 0
|
|
104
|
+
device = devices[0]
|
|
105
|
+
assert len(device.players) > 0
|
|
106
|
+
|
|
107
|
+
getattr(mock_api, mock_api_function).side_effect = side_effect
|
|
108
|
+
|
|
109
|
+
await device.get_monthly_summary()
|
|
110
|
+
assert expected_log.format(DEVICE_ID=device.device_id) in caplog.text
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Tests for the pynintendoparental package."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pynintendoauth.exceptions import HttpException
|
|
7
|
+
|
|
8
|
+
from pynintendoparental import NintendoParental, NoDevicesFoundException
|
|
9
|
+
from pynintendoparental.authenticator import Authenticator
|
|
10
|
+
|
|
11
|
+
from .helpers import load_fixture
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(name="mock_api_init")
|
|
15
|
+
def fixture_mock_api_init():
|
|
16
|
+
"""Fixture to mock the Api class."""
|
|
17
|
+
with patch("pynintendoparental.Api", autospec=True) as mock_api:
|
|
18
|
+
yield mock_api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def test_create_method(
|
|
22
|
+
mock_authenticator: Authenticator, mock_api_init: AsyncMock
|
|
23
|
+
):
|
|
24
|
+
"""Test the create class method."""
|
|
25
|
+
devices_fixture = await load_fixture("account_devices")
|
|
26
|
+
device_id = devices_fixture["ownedDevices"][0]["deviceId"]
|
|
27
|
+
mock_api_instance = mock_api_init.return_value
|
|
28
|
+
mock_api_instance.async_get_account_devices.return_value = {"json": devices_fixture}
|
|
29
|
+
|
|
30
|
+
parental = await NintendoParental.create(mock_authenticator)
|
|
31
|
+
|
|
32
|
+
assert isinstance(parental, NintendoParental)
|
|
33
|
+
assert parental.account_id == mock_authenticator.account_id
|
|
34
|
+
assert len(parental.devices) == 1
|
|
35
|
+
assert device_id in parental.devices
|
|
36
|
+
mock_api_init.assert_called_once()
|
|
37
|
+
mock_api_instance.async_get_account_devices.assert_called_once()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def test_no_devices_found(
|
|
41
|
+
mock_authenticator: Authenticator, mock_api_init: AsyncMock
|
|
42
|
+
):
|
|
43
|
+
"""Test the create class method when no devices are found."""
|
|
44
|
+
mock_api_instance = mock_api_init.return_value
|
|
45
|
+
mock_api_instance.async_get_account_devices.side_effect = HttpException(
|
|
46
|
+
404, "Not Found"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
with pytest.raises(NoDevicesFoundException):
|
|
50
|
+
await NintendoParental.create(mock_authenticator)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def test_device_fetch_http_exception(
|
|
54
|
+
mock_authenticator: Authenticator, mock_api_init: AsyncMock
|
|
55
|
+
):
|
|
56
|
+
"""Test an HttpException when fetching devices."""
|
|
57
|
+
mock_api_instance = mock_api_init.return_value
|
|
58
|
+
mock_api_instance.async_get_account_devices.side_effect = HttpException(
|
|
59
|
+
500, "Internal Server Error"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
with pytest.raises(HttpException):
|
|
63
|
+
await NintendoParental.create(mock_authenticator)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def test_device_update_exception(
|
|
67
|
+
mock_authenticator: Authenticator, mock_api_init: AsyncMock
|
|
68
|
+
):
|
|
69
|
+
"""Test an exception during device update doesn't stop creation."""
|
|
70
|
+
devices_fixture = await load_fixture("account_devices")
|
|
71
|
+
device_id = devices_fixture["ownedDevices"][0]["deviceId"]
|
|
72
|
+
mock_api_instance = mock_api_init.return_value
|
|
73
|
+
mock_api_instance.async_get_account_devices.return_value = {"json": devices_fixture}
|
|
74
|
+
with patch(
|
|
75
|
+
"pynintendoparental.device.Device.update",
|
|
76
|
+
new=AsyncMock(side_effect=Exception("Update Failed")),
|
|
77
|
+
):
|
|
78
|
+
parental = await NintendoParental.create(mock_authenticator)
|
|
79
|
+
assert len(parental.devices) == 1
|
|
80
|
+
assert device_id in parental.devices
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Tests for the Player class."""
|
|
2
|
+
|
|
3
|
+
from syrupy.assertion import SnapshotAssertion
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
from pynintendoparental.player import Player
|
|
7
|
+
|
|
8
|
+
from .helpers import load_fixture
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def test_player_parsing(snapshot: SnapshotAssertion):
|
|
12
|
+
"""Test that the player class parsing works as expected."""
|
|
13
|
+
daily_summaries_response = await load_fixture("device_daily_summaries")
|
|
14
|
+
players = Player.from_device_daily_summary(
|
|
15
|
+
daily_summaries_response["dailySummaries"]
|
|
16
|
+
)
|
|
17
|
+
assert len(players) > 0
|
|
18
|
+
player = players[0]
|
|
19
|
+
|
|
20
|
+
assert player == snapshot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def test_player_update_from_daily_summary(snapshot: SnapshotAssertion):
|
|
24
|
+
"""Test that updating a player from a daily summary works."""
|
|
25
|
+
daily_summaries_response = await load_fixture("device_daily_summaries")
|
|
26
|
+
players = Player.from_device_daily_summary(
|
|
27
|
+
daily_summaries_response["dailySummaries"]
|
|
28
|
+
)
|
|
29
|
+
assert len(players) > 0
|
|
30
|
+
player = players[0]
|
|
31
|
+
|
|
32
|
+
# Create a deep copy to modify for the update
|
|
33
|
+
updated_summary = copy.deepcopy(daily_summaries_response)
|
|
34
|
+
|
|
35
|
+
# Find the corresponding player in the new summary and update their data
|
|
36
|
+
for p_summary in updated_summary["dailySummaries"][0]["players"]:
|
|
37
|
+
if p_summary["profile"]["playerId"] == player.player_id:
|
|
38
|
+
p_summary["playingTime"] = 54321
|
|
39
|
+
p_summary["playedGames"] = [{"app": "new_app"}]
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
player.update_from_daily_summary(updated_summary["dailySummaries"])
|
|
43
|
+
|
|
44
|
+
assert player.playing_time == 54321
|
|
45
|
+
assert player.apps == [{"app": "new_app"}]
|
|
46
|
+
assert player == snapshot
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pynintendoparental-2.0.0 → pynintendoparental-2.1.1}/pynintendoparental.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|