pynintendoparental 2.3.0__tar.gz → 2.3.2.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.
Files changed (31) hide show
  1. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/PKG-INFO +6 -6
  2. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/__init__.py +39 -4
  3. pynintendoparental-2.3.2.1/pynintendoparental/_version.py +1 -0
  4. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/api.py +18 -43
  5. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/application.py +66 -24
  6. pynintendoparental-2.3.2.1/pynintendoparental/authenticator.py +44 -0
  7. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/const.py +5 -3
  8. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/device.py +410 -178
  9. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/player.py +33 -4
  10. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental.egg-info/PKG-INFO +6 -6
  11. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental.egg-info/requires.txt +5 -5
  12. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/setup.py +5 -5
  13. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/tests/test_api.py +10 -30
  14. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/tests/test_applications.py +7 -15
  15. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/tests/test_device.py +134 -81
  16. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/tests/test_init.py +6 -18
  17. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/tests/test_player.py +4 -7
  18. pynintendoparental-2.3.0/pynintendoparental/_version.py +0 -1
  19. pynintendoparental-2.3.0/pynintendoparental/authenticator.py +0 -18
  20. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/LICENSE +0 -0
  21. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/README.md +0 -0
  22. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/enum.py +0 -0
  23. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/exceptions.py +0 -0
  24. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/py.typed +0 -0
  25. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental/utils.py +0 -0
  26. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental.egg-info/SOURCES.txt +0 -0
  27. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental.egg-info/dependency_links.txt +0 -0
  28. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pynintendoparental.egg-info/top_level.txt +0 -0
  29. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/pyproject.toml +0 -0
  30. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/setup.cfg +0 -0
  31. {pynintendoparental-2.3.0 → pynintendoparental-2.3.2.1}/tests/test_enum.py +1 -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 2.3.0
3
+ Version: 2.3.2.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
@@ -14,14 +14,14 @@ License-File: LICENSE
14
14
  Requires-Dist: pynintendoauth==1.0.2
15
15
  Provides-Extra: dev
16
16
  Requires-Dist: aiofiles<26,>=23; extra == "dev"
17
- Requires-Dist: bandit<1.9,>=1.7; extra == "dev"
17
+ Requires-Dist: bandit<1.10,>=1.7; extra == "dev"
18
18
  Requires-Dist: black<26,>=23; extra == "dev"
19
- Requires-Dist: build<1.4,>=0.10; extra == "dev"
20
- Requires-Dist: Faker<39,>=38; extra == "dev"
19
+ Requires-Dist: build<1.5,>=0.10; extra == "dev"
20
+ Requires-Dist: Faker<41,>=38; extra == "dev"
21
21
  Requires-Dist: flake8<8,>=6; extra == "dev"
22
- Requires-Dist: isort<7,>=5; extra == "dev"
22
+ Requires-Dist: isort<8,>=5; extra == "dev"
23
23
  Requires-Dist: mypy<1.20,>=1.5; extra == "dev"
24
- Requires-Dist: pytest<9,>=7; extra == "dev"
24
+ Requires-Dist: pytest<10,>=7; extra == "dev"
25
25
  Requires-Dist: pytest-cov<8,>=4; extra == "dev"
26
26
  Requires-Dist: pytest-asyncio<2.0,>=0.21; extra == "dev"
27
27
  Requires-Dist: syrupy<6,>=5; extra == "dev"
@@ -6,14 +6,22 @@ import asyncio
6
6
  from pynintendoauth.exceptions import HttpException
7
7
 
8
8
  from .api import Api
9
+ from .authenticator import Authenticator
9
10
  from .const import _LOGGER
10
11
  from .device import Device
11
12
  from .exceptions import NoDevicesFoundException
12
- from .authenticator import Authenticator
13
13
 
14
14
 
15
15
  class NintendoParental:
16
- """Core Python API."""
16
+ """Core Python API for Nintendo Switch Parental Controls.
17
+
18
+ This is the main entry point for interacting with Nintendo Switch Parental Controls.
19
+ Use the `create` class method to instantiate this class.
20
+
21
+ Attributes:
22
+ account_id: The Nintendo account ID.
23
+ devices: Dictionary of Device objects keyed by device ID.
24
+ """
17
25
 
18
26
  def __init__(self, auth: Authenticator, timezone, lang) -> None:
19
27
  self._api: Api = Api(auth=auth, tz=timezone, lang=lang)
@@ -48,7 +56,12 @@ class NintendoParental:
48
56
  _LOGGER.debug("Found %s device(s)", len(self.devices))
49
57
 
50
58
  async def update(self):
51
- """Update module data."""
59
+ """Update module data.
60
+
61
+ Refreshes all devices and their associated data from Nintendo's servers.
62
+ This method fetches the latest information about all devices linked to
63
+ the authenticated account.
64
+ """
52
65
  _LOGGER.debug("Received request to update data.")
53
66
  await self._get_devices()
54
67
  _LOGGER.debug("Update complete.")
@@ -57,7 +70,29 @@ class NintendoParental:
57
70
  async def create(
58
71
  cls, auth: Authenticator, timezone: str = "Europe/London", lang: str = "en-GB"
59
72
  ) -> "NintendoParental":
60
- """Create an instance of NintendoParental."""
73
+ """Create an instance of NintendoParental.
74
+
75
+ This is the recommended way to create a NintendoParental instance as it
76
+ handles the asynchronous initialization and fetches initial device data.
77
+
78
+ Args:
79
+ auth: An authenticated Authenticator instance.
80
+ timezone: The timezone to use for API requests (default: "Europe/London").
81
+ Use any valid IANA timezone identifier.
82
+ lang: The language code for API responses (default: "en-GB").
83
+ Use ISO 639-1 language codes with ISO 3166-1 country codes.
84
+
85
+ Returns:
86
+ A fully initialized NintendoParental instance with device data loaded.
87
+
88
+ Example:
89
+ ```python
90
+ async with aiohttp.ClientSession() as session:
91
+ auth = Authenticator(session_token, session)
92
+ await auth.async_complete_login(use_session_token=True)
93
+ nintendo = await NintendoParental.create(auth, timezone="America/New_York", lang="en-US")
94
+ ```
95
+ """
61
96
  self = cls(auth, timezone, lang)
62
97
  await self.update()
63
98
  return self
@@ -0,0 +1 @@
1
+ __version__ = "2.3.2.1"
@@ -1,21 +1,20 @@
1
1
  """API handler."""
2
2
 
3
3
  import aiohttp
4
-
5
4
  from pynintendoauth.exceptions import HttpException
6
5
 
7
6
  from .authenticator import Authenticator
8
7
  from .const import (
9
- ENDPOINTS,
8
+ _LOGGER,
10
9
  BASE_URL,
11
- USER_AGENT,
12
- MOBILE_APP_PKG,
10
+ DEVICE_MODEL,
11
+ ENDPOINTS,
13
12
  MOBILE_APP_BUILD,
13
+ MOBILE_APP_PKG,
14
14
  MOBILE_APP_VERSION,
15
- OS_VERSION,
16
15
  OS_NAME,
17
- DEVICE_MODEL,
18
- _LOGGER,
16
+ OS_VERSION,
17
+ USER_AGENT,
19
18
  )
20
19
 
21
20
 
@@ -80,9 +79,7 @@ class Api:
80
79
  try:
81
80
  error: dict = await response.json()
82
81
  if "detail" in error:
83
- raise HttpException(
84
- response.status, error["detail"], error.get("errorCode")
85
- )
82
+ raise HttpException(response.status, error["detail"], error.get("errorCode"))
86
83
  except (aiohttp.ContentTypeError, ValueError):
87
84
  # Fall through to the generic exception below on parsing failure.
88
85
  pass
@@ -114,31 +111,21 @@ class Api:
114
111
 
115
112
  async def async_get_account_device(self, device_id: str) -> dict:
116
113
  """Get account device."""
117
- return await self.send_request(
118
- endpoint="get_account_device", DEVICE_ID=device_id
119
- )
114
+ return await self.send_request(endpoint="get_account_device", DEVICE_ID=device_id)
120
115
 
121
116
  async def async_get_device_daily_summaries(self, device_id: str) -> dict:
122
117
  """Get device daily summaries."""
123
- return await self.send_request(
124
- endpoint="get_device_daily_summaries", DEVICE_ID=device_id
125
- )
118
+ return await self.send_request(endpoint="get_device_daily_summaries", DEVICE_ID=device_id)
126
119
 
127
120
  async def async_get_device_monthly_summaries(self, device_id: str) -> dict:
128
121
  """Get device monthly summaries."""
129
- return await self.send_request(
130
- endpoint="get_device_monthly_summaries", DEVICE_ID=device_id
131
- )
122
+ return await self.send_request(endpoint="get_device_monthly_summaries", DEVICE_ID=device_id)
132
123
 
133
124
  async def async_get_device_parental_control_setting(self, device_id: str) -> dict:
134
125
  """Get device parental control setting."""
135
- return await self.send_request(
136
- endpoint="get_device_parental_control_setting", DEVICE_ID=device_id
137
- )
126
+ return await self.send_request(endpoint="get_device_parental_control_setting", DEVICE_ID=device_id)
138
127
 
139
- async def async_get_device_monthly_summary(
140
- self, device_id: str, year: int, month: int
141
- ) -> dict:
128
+ async def async_get_device_monthly_summary(self, device_id: str, year: int, month: int) -> dict:
142
129
  """Get device monthly summary."""
143
130
  return await self.send_request(
144
131
  endpoint="get_device_monthly_summary",
@@ -147,9 +134,7 @@ class Api:
147
134
  MONTH=f"{month:02d}",
148
135
  )
149
136
 
150
- async def async_update_restriction_level(
151
- self, device_id: str, parental_control_setting: dict
152
- ) -> dict:
137
+ async def async_update_restriction_level(self, device_id: str, parental_control_setting: dict) -> dict:
153
138
  """Update device restriction level."""
154
139
  allowed_keys = (
155
140
  "whitelistedApplicationList",
@@ -159,18 +144,12 @@ class Api:
159
144
  "deviceId": device_id,
160
145
  "customSettings": parental_control_setting.get("customSettings", {}),
161
146
  "parentalControlSettingEtag": parental_control_setting.get("etag"),
162
- "vrRestrictionEtag": parental_control_setting.get("customSettings", {}).get(
163
- "vrRestrictionEtag"
164
- ),
147
+ "vrRestrictionEtag": parental_control_setting.get("customSettings", {}).get("vrRestrictionEtag"),
165
148
  **{key: parental_control_setting.get(key) for key in allowed_keys},
166
149
  }
167
- return await self.send_request(
168
- endpoint="update_restriction_level", body=settings
169
- )
150
+ return await self.send_request(endpoint="update_restriction_level", body=settings)
170
151
 
171
- async def async_update_play_timer(
172
- self, device_id: str, play_timer_regulations: dict
173
- ) -> dict:
152
+ async def async_update_play_timer(self, device_id: str, play_timer_regulations: dict) -> dict:
174
153
  """Update device play timer settings."""
175
154
  allowed_ptr_keys = (
176
155
  "timerMode",
@@ -180,9 +159,7 @@ class Api:
180
159
  )
181
160
  settings = {
182
161
  "deviceId": device_id,
183
- "playTimerRegulations": {
184
- key: play_timer_regulations.get(key) for key in allowed_ptr_keys
185
- },
162
+ "playTimerRegulations": {key: play_timer_regulations.get(key) for key in allowed_ptr_keys},
186
163
  }
187
164
  return await self.send_request(endpoint="update_play_timer", body=settings)
188
165
 
@@ -193,9 +170,7 @@ class Api:
193
170
  body={"deviceId": device_id, "unlockCode": str(new_code)},
194
171
  )
195
172
 
196
- async def async_update_extra_playing_time(
197
- self, device_id: str, additional_time: int
198
- ) -> dict:
173
+ async def async_update_extra_playing_time(self, device_id: str, additional_time: int) -> dict:
199
174
  """Update device extra playing time."""
200
175
  body = {
201
176
  "deviceId": device_id,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import copy
4
4
  from datetime import datetime
5
- from typing import Callable, TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Callable
6
6
 
7
7
  from .api import Api
8
8
  from .const import _LOGGER
@@ -14,7 +14,20 @@ if TYPE_CHECKING:
14
14
 
15
15
 
16
16
  class Application:
17
- """Model for an application"""
17
+ """A Nintendo Switch game or application.
18
+
19
+ Represents a game or application on a Nintendo Switch console with parental control settings.
20
+
21
+ Attributes:
22
+ application_id: Unique identifier for the application.
23
+ name: Display name of the application.
24
+ image_url: URL to the application's icon image.
25
+ safe_launch_setting: Whether the app is on the Allow List (bypasses age restrictions).
26
+ today_time_played: Total time played today across all players in minutes.
27
+ first_played_date: Date when the application was first played.
28
+ playing_days: Number of days the application has been played.
29
+ shop_url: URL to the application in the Nintendo eShop.
30
+ """
18
31
 
19
32
  def __init__(
20
33
  self,
@@ -48,11 +61,27 @@ class Application:
48
61
  callbacks.append(self._internal_update_callback)
49
62
 
50
63
  async def set_safe_launch_setting(self, safe_launch_setting: SafeLaunchSetting):
51
- """Set the safe launch setting for the application."""
52
- if (
53
- not self._device
54
- or "whitelistedApplicationList" not in self._parental_control_settings
55
- ):
64
+ """Set the application's status on the Allow List.
65
+
66
+ Applications on the Allow List can bypass general age/content restrictions.
67
+
68
+ Args:
69
+ safe_launch_setting: The setting to apply. Options are:
70
+ - SafeLaunchSetting.NONE: Remove from Allow List (apply normal restrictions).
71
+ - SafeLaunchSetting.ALLOW: Add to Allow List (bypass restrictions).
72
+
73
+ Raises:
74
+ ValueError: If the application data is not properly initialized.
75
+ LookupError: If the application is no longer in the whitelist.
76
+
77
+ Example:
78
+ ```python
79
+ from pynintendoparental.enum import SafeLaunchSetting
80
+
81
+ await app.set_safe_launch_setting(SafeLaunchSetting.ALLOW)
82
+ ```
83
+ """
84
+ if not self._device or "whitelistedApplicationList" not in self._parental_control_settings:
56
85
  raise ValueError("Unable to set SafeLaunchSetting, callbacks not executed.")
57
86
  # Update the application safe_launch_setting in the PCS
58
87
  pcs = copy.deepcopy(self._parental_control_settings)
@@ -61,9 +90,7 @@ class Application:
61
90
  app["safeLaunch"] = str(safe_launch_setting)
62
91
  break
63
92
  else:
64
- raise LookupError(
65
- "Unable to set SafeLaunchSetting, application no longer in whitelist."
66
- )
93
+ raise LookupError("Unable to set SafeLaunchSetting, application no longer in whitelist.")
67
94
 
68
95
  await self._send_api_update(
69
96
  self._api.async_update_restriction_level,
@@ -92,23 +119,16 @@ class Application:
92
119
  device.device_id,
93
120
  self.application_id,
94
121
  )
95
- for app in self._parental_control_settings.get(
96
- "whitelistedApplicationList", []
97
- ):
122
+ for app in self._parental_control_settings.get("whitelistedApplicationList", []):
98
123
  if app["applicationId"].upper() == self.application_id.upper():
99
- self.safe_launch_setting = SafeLaunchSetting(
100
- app.get("safeLaunch", "NONE")
101
- )
124
+ self.safe_launch_setting = SafeLaunchSetting(app.get("safeLaunch", "NONE"))
102
125
  self.image_url = app["imageUri"]
103
126
  break
104
127
  total_time_played: int = 0
105
128
  if self._daily_summary:
106
129
  for player_summary in self._daily_summary[0].get("players", []):
107
130
  for player_app in player_summary.get("playedGames", []):
108
- if (
109
- player_app["meta"]["applicationId"].upper()
110
- == self.application_id.upper()
111
- ):
131
+ if player_app["meta"]["applicationId"].upper() == self.application_id.upper():
112
132
  total_time_played += player_app["playingTime"]
113
133
  break
114
134
  self.today_time_played = total_time_played
@@ -119,14 +139,36 @@ class Application:
119
139
  else:
120
140
  cb(self)
121
141
 
122
- def add_application_callback(self, callback):
123
- """Add a callback to the application."""
142
+ def add_application_callback(self, callback: Callable):
143
+ """Add a callback function to be called when application state changes.
144
+
145
+ Args:
146
+ callback: A callable function. Can be sync or async.
147
+
148
+ Raises:
149
+ ValueError: If the provided object is not callable.
150
+
151
+ Example:
152
+ ```python
153
+ async def on_app_update(app):
154
+ print(f"App {app.name} updated!")
155
+
156
+ app.add_application_callback(on_app_update)
157
+ ```
158
+ """
124
159
  if not callable(callback):
125
160
  raise ValueError("Object must be callable.")
126
161
  self._callbacks.append(callback)
127
162
 
128
- def remove_application_callback(self, callback):
129
- """Remove a callback from the application."""
163
+ def remove_application_callback(self, callback: Callable):
164
+ """Remove a previously registered application callback.
165
+
166
+ Args:
167
+ callback: The callback function to remove.
168
+
169
+ Raises:
170
+ ValueError: If the callback is not found.
171
+ """
130
172
  if callback not in self._callbacks:
131
173
  raise ValueError("Callback not found.")
132
174
  self._callbacks.remove(callback)
@@ -0,0 +1,44 @@
1
+ """Nintendo Authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pynintendoauth import NintendoAuth
6
+
7
+ from .const import CLIENT_ID
8
+
9
+
10
+ class Authenticator(NintendoAuth):
11
+ """Nintendo authentication handler.
12
+
13
+ Handles authentication with Nintendo's servers for accessing Parental Controls API.
14
+ Supports both session token and interactive login methods.
15
+
16
+ Attributes:
17
+ session_token: The session token for authentication.
18
+ account_id: The authenticated Nintendo account ID.
19
+ login_url: URL for interactive login (when session_token not provided).
20
+
21
+ Example:
22
+ Using a session token:
23
+ ```python
24
+ async with aiohttp.ClientSession() as session:
25
+ auth = Authenticator(session_token="YOUR_TOKEN", client_session=session)
26
+ await auth.async_complete_login(use_session_token=True)
27
+ ```
28
+
29
+ Interactive login:
30
+ ```python
31
+ async with aiohttp.ClientSession() as session:
32
+ auth = Authenticator(client_session=session)
33
+ print(f"Visit: {auth.login_url}")
34
+ response_url = input("Paste redirect URL: ")
35
+ await auth.async_complete_login(response_url)
36
+ ```
37
+ """
38
+
39
+ def __init__(self, session_token=None, client_session=None):
40
+ super().__init__(
41
+ client_id=CLIENT_ID,
42
+ session_token=session_token,
43
+ client_session=client_session,
44
+ )
@@ -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.3.1"
10
- MOBILE_APP_BUILD = "620"
9
+ MOBILE_APP_VERSION = "2.3.2"
10
+ MOBILE_APP_BUILD = "640"
11
11
  OS_NAME = "ANDROID"
12
12
  OS_VERSION = "34"
13
13
  OS_STR = f"{OS_NAME} {OS_VERSION}"
@@ -59,7 +59,9 @@ ENDPOINTS = {
59
59
  "method": "POST",
60
60
  },
61
61
  "get_device_monthly_summary": {
62
- "url": "{BASE_URL}/v2/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false",
62
+ "url": (
63
+ "{BASE_URL}/v2/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false"
64
+ ),
63
65
  "method": "GET",
64
66
  },
65
67
  "update_extra_playing_time": {