pynintendoparental 1.1.2__tar.gz → 2.0.0__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 (26) hide show
  1. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/PKG-INFO +3 -1
  2. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/__init__.py +11 -13
  3. pynintendoparental-2.0.0/pynintendoparental/_version.py +1 -0
  4. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/api.py +68 -84
  5. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/application.py +17 -11
  6. pynintendoparental-2.0.0/pynintendoparental/authenticator.py +18 -0
  7. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/const.py +21 -12
  8. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/device.py +300 -204
  9. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/enum.py +15 -1
  10. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/exceptions.py +4 -17
  11. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/player.py +3 -1
  12. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/utils.py +1 -0
  13. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental.egg-info/PKG-INFO +3 -1
  14. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental.egg-info/SOURCES.txt +2 -3
  15. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental.egg-info/requires.txt +1 -0
  16. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/setup.py +1 -0
  17. pynintendoparental-1.1.2/pynintendoparental/_version.py +0 -1
  18. pynintendoparental-1.1.2/pynintendoparental/authenticator/__init__.py +0 -226
  19. pynintendoparental-1.1.2/pynintendoparental/authenticator/const.py +0 -29
  20. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/LICENSE +0 -0
  21. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/README.md +0 -0
  22. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental/py.typed +0 -0
  23. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental.egg-info/dependency_links.txt +0 -0
  24. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pynintendoparental.egg-info/top_level.txt +0 -0
  25. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/pyproject.toml +0 -0
  26. {pynintendoparental-1.1.2 → pynintendoparental-2.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 1.1.2
3
+ Version: 2.0.0
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,6 +11,7 @@ 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~=1.0.0
14
15
  Provides-Extra: dev
15
16
  Requires-Dist: bandit<1.9,>=1.7; extra == "dev"
16
17
  Requires-Dist: black<26,>=23; extra == "dev"
@@ -29,6 +30,7 @@ Dynamic: home-page
29
30
  Dynamic: license
30
31
  Dynamic: license-file
31
32
  Dynamic: provides-extra
33
+ Dynamic: requires-dist
32
34
  Dynamic: requires-python
33
35
  Dynamic: summary
34
36
 
@@ -3,34 +3,33 @@
3
3
 
4
4
  import asyncio
5
5
 
6
- from pynintendoparental.exceptions import HttpException, NoDevicesFoundException
6
+ from pynintendoauth.exceptions import HttpException
7
7
 
8
- from .authenticator import Authenticator
9
8
  from .api import Api
10
9
  from .const import _LOGGER
11
10
  from .device import Device
11
+ from .exceptions import NoDevicesFoundException
12
+ from .authenticator import Authenticator
13
+
12
14
 
13
15
  class NintendoParental:
14
16
  """Core Python API."""
15
17
 
16
- def __init__(self,
17
- auth: Authenticator,
18
- timezone,
19
- lang) -> None:
18
+ def __init__(self, auth: Authenticator, timezone, lang) -> None:
20
19
  self._api: Api = Api(auth=auth, tz=timezone, lang=lang)
21
20
  self.account_id = auth.account_id
22
21
  self.devices: dict[str, Device] = {}
23
22
 
24
23
  async def _get_devices(self):
25
24
  """Gets devices from the API and stores in self.devices"""
25
+
26
26
  async def update_device(dev: Device):
27
27
  """Update a device."""
28
28
  try:
29
29
  await dev.update()
30
30
  except Exception as err:
31
- _LOGGER.exception("Error updating device %s: %s",
32
- dev.device_id,
33
- err)
31
+ _LOGGER.exception("Error updating device %s: %s", dev.device_id, err)
32
+
34
33
  try:
35
34
  response = await self._api.async_get_account_devices()
36
35
  except HttpException as err:
@@ -55,10 +54,9 @@ class NintendoParental:
55
54
  _LOGGER.debug("Update complete.")
56
55
 
57
56
  @classmethod
58
- async def create(cls,
59
- auth: Authenticator,
60
- timezone: str = "Europe/London",
61
- lang: str = "en-GB") -> 'NintendoParental':
57
+ async def create(
58
+ cls, auth: Authenticator, timezone: str = "Europe/London", lang: str = "en-GB"
59
+ ) -> "NintendoParental":
62
60
  """Create an instance of NintendoParental."""
63
61
  self = cls(auth, timezone, lang)
64
62
  await self.update()
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
@@ -2,6 +2,8 @@
2
2
 
3
3
  import aiohttp
4
4
 
5
+ from pynintendoauth.exceptions import HttpException
6
+
5
7
  from .authenticator import Authenticator
6
8
  from .const import (
7
9
  ENDPOINTS,
@@ -13,13 +15,14 @@ from .const import (
13
15
  OS_VERSION,
14
16
  OS_NAME,
15
17
  DEVICE_MODEL,
16
- _LOGGER
18
+ _LOGGER,
17
19
  )
18
- from .exceptions import HttpException
20
+
19
21
 
20
22
  def _check_http_success(status: int) -> bool:
21
23
  return status >= 200 and status < 300
22
24
 
25
+
23
26
  class Api:
24
27
  """Nintendo Parental Controls API."""
25
28
 
@@ -48,10 +51,10 @@ class Api:
48
51
  "X-Moon-TimeZone": self._tz,
49
52
  "X-Moon-Os-Language": self._language,
50
53
  "X-Moon-App-Language": self._language,
51
- "Authorization": self._auth.access_token
54
+ "Authorization": self._auth.access_token,
52
55
  }
53
56
 
54
- async def send_request(self, endpoint: str, body: object=None, **kwargs):
57
+ async def send_request(self, endpoint: str, body: object = None, **kwargs):
55
58
  """Sends a request to a given endpoint."""
56
59
  _LOGGER.debug("Sending request to %s", endpoint)
57
60
  # Get the endpoint from the endpoints map
@@ -66,38 +69,45 @@ class Api:
66
69
  url = e_point.get("url").format(BASE_URL=BASE_URL, **kwargs)
67
70
  _LOGGER.debug("Built URL %s", url)
68
71
  # now send the HTTP request
69
- resp: dict = {
70
- "status": 0,
71
- "text": "",
72
- "json": "",
73
- "headers": ""
74
- }
75
- async with self._auth.client_session.request(
76
- method=e_point.get("method"),
77
- url=url,
78
- json=body,
79
- headers=self._headers
80
- ) as response:
81
- _LOGGER.debug("%s request to %s status code %s",
82
- e_point.get("method"),
83
- url,
84
- response.status)
85
- if _check_http_success(response.status):
86
- resp["status"] = response.status
87
- resp["text"] = await response.text()
72
+ resp: dict = {"status": 0, "text": "", "json": "", "headers": ""}
73
+ response = await self._auth.async_authenticated_request(
74
+ method=e_point.get("method"), url=url, headers=self._headers, body=body
75
+ )
76
+ _LOGGER.debug(
77
+ "%s request to %s status code %s",
78
+ e_point.get("method"),
79
+ url,
80
+ response.status,
81
+ )
82
+ if not _check_http_success(response.status):
83
+ if response.content_type == "application/problem+json":
88
84
  try:
89
- resp["json"] = await response.json()
90
- except (aiohttp.ContentTypeError, ValueError) as e:
91
- _LOGGER.warning(
92
- """Failed to decode JSON response from %s.
93
- Status: %s, Error: %s.
94
- Response text: %s...""",
95
- url, response.status, e, resp['text'][:200]
96
- )
97
- resp["json"] = {}
98
- resp["headers"] = response.headers
99
- else:
100
- raise HttpException(response.status, await response.text())
85
+ error: dict = await response.json()
86
+ if "detail" in error:
87
+ raise HttpException(
88
+ response.status, error["detail"], error.get("errorCode")
89
+ )
90
+ except (aiohttp.ContentTypeError, ValueError):
91
+ # Fall through to the generic exception below on parsing failure.
92
+ pass
93
+ raise HttpException(response.status, await response.text())
94
+
95
+ resp["status"] = response.status
96
+ resp["text"] = await response.text()
97
+ try:
98
+ resp["json"] = await response.json()
99
+ except (aiohttp.ContentTypeError, ValueError) as e:
100
+ _LOGGER.warning(
101
+ """Failed to decode JSON response from %s.
102
+ Status: %s, Error: %s.
103
+ Response text: %s...""",
104
+ url,
105
+ response.status,
106
+ e,
107
+ resp["text"][:200],
108
+ )
109
+ resp["json"] = {}
110
+ resp["headers"] = response.headers
101
111
 
102
112
  # now return the resp dict
103
113
  return resp
@@ -105,109 +115,83 @@ class Api:
105
115
  async def async_get_account_details(self) -> dict:
106
116
  """Get account details."""
107
117
  return await self.send_request(
108
- endpoint="get_account_details",
109
- ACCOUNT_ID=self.account_id
118
+ endpoint="get_account_details", ACCOUNT_ID=self.account_id
110
119
  )
111
120
 
112
121
  async def async_get_account_devices(self) -> dict:
113
122
  """Get account devices."""
114
- return await self.send_request(
115
- endpoint="get_account_devices"
116
- )
123
+ return await self.send_request(endpoint="get_account_devices")
117
124
 
118
125
  async def async_get_account_device(self, device_id: str) -> dict:
119
126
  """Get account device."""
120
127
  return await self.send_request(
121
- endpoint="get_account_device",
122
- DEVICE_ID=device_id
128
+ endpoint="get_account_device", DEVICE_ID=device_id
123
129
  )
124
130
 
125
131
  async def async_get_device_daily_summaries(self, device_id: str) -> dict:
126
132
  """Get device daily summaries."""
127
133
  return await self.send_request(
128
- endpoint="get_device_daily_summaries",
129
- DEVICE_ID=device_id
134
+ endpoint="get_device_daily_summaries", DEVICE_ID=device_id
130
135
  )
131
136
 
132
137
  async def async_get_device_monthly_summaries(self, device_id: str) -> dict:
133
138
  """Get device monthly summaries."""
134
139
  return await self.send_request(
135
- endpoint="get_device_monthly_summaries",
136
- DEVICE_ID=device_id
140
+ endpoint="get_device_monthly_summaries", DEVICE_ID=device_id
137
141
  )
138
142
 
139
143
  async def async_get_device_parental_control_setting(self, device_id: str) -> dict:
140
144
  """Get device parental control setting."""
141
145
  return await self.send_request(
142
- endpoint="get_device_parental_control_setting",
143
- DEVICE_ID=device_id
146
+ endpoint="get_device_parental_control_setting", DEVICE_ID=device_id
144
147
  )
145
148
 
146
- async def async_get_device_parental_control_setting_state(self, device_id: str) -> dict:
149
+ async def async_get_device_parental_control_setting_state(
150
+ self, device_id: str
151
+ ) -> dict:
147
152
  """Get device parental control setting state."""
148
153
  return await self.send_request(
149
- endpoint="get_device_parental_control_setting_state",
150
- DEVICE_ID=device_id
154
+ endpoint="get_device_parental_control_setting_state", DEVICE_ID=device_id
151
155
  )
152
156
 
153
- async def async_get_device_monthly_summary(self, device_id: str, year: int, month: int) -> dict:
157
+ async def async_get_device_monthly_summary(
158
+ self, device_id: str, year: int, month: int
159
+ ) -> dict:
154
160
  """Get device monthly summary."""
155
161
  return await self.send_request(
156
162
  endpoint="get_device_monthly_summary",
157
163
  DEVICE_ID=device_id,
158
164
  YEAR=year,
159
- MONTH=f"{month:02d}"
165
+ MONTH=f"{month:02d}",
160
166
  )
161
167
 
162
- async def async_update_restriction_level(
163
- self,
164
- settings: dict
165
- ) -> dict:
168
+ async def async_update_restriction_level(self, settings: dict) -> dict:
166
169
  """Update device restriction level."""
167
170
  return await self.send_request(
168
- endpoint="update_restriction_level",
169
- body=settings
171
+ endpoint="update_restriction_level", body=settings
170
172
  )
171
173
 
172
- async def async_update_play_timer(
173
- self,
174
- settings: dict
175
- ) -> dict:
174
+ async def async_update_play_timer(self, settings: dict) -> dict:
176
175
  """Update device play timer settings."""
177
- return await self.send_request(
178
- endpoint="update_play_timer",
179
- body=settings
180
- )
176
+ return await self.send_request(endpoint="update_play_timer", body=settings)
181
177
 
182
- async def async_update_unlock_code(
183
- self,
184
- new_code: str,
185
- device_id: str
186
- ) -> dict:
178
+ async def async_update_unlock_code(self, new_code: str, device_id: str) -> dict:
187
179
  """Update device unlock code."""
188
180
  return await self.send_request(
189
181
  endpoint="update_unlock_code",
190
- body={
191
- "deviceId": device_id,
192
- "unlockCode": new_code
193
- }
182
+ body={"deviceId": device_id, "unlockCode": new_code},
194
183
  )
195
184
 
196
185
  async def async_update_extra_playing_time(
197
- self,
198
- device_id: str,
199
- additional_time: int
186
+ self, device_id: str, additional_time: int
200
187
  ) -> dict:
201
188
  """Update device extra playing time."""
202
189
  body = {
203
190
  "deviceId": device_id,
204
191
  "additionalTime": additional_time,
205
- "status": "TO_ADDED"
192
+ "status": "TO_ADDED",
206
193
  }
207
194
  if additional_time == -1:
208
195
  body["status"] = "TO_INFINITY"
209
196
  body.pop("additionalTime")
210
- return await self.send_request(
211
- endpoint="update_extra_playing_time",
212
- body=body
213
- )
197
+ return await self.send_request(endpoint="update_extra_playing_time", body=body)
@@ -4,6 +4,7 @@ from datetime import datetime
4
4
 
5
5
  from .const import _LOGGER
6
6
 
7
+
7
8
  class Application:
8
9
  """Model for an application"""
9
10
 
@@ -12,7 +13,7 @@ class Application:
12
13
  self.application_id: str = None
13
14
  self.first_played_date: datetime = None
14
15
  self.has_ugc: bool = None
15
- self.image_url: str = None # uses small image from Nintendo
16
+ self.image_url: str = None # uses small image from Nintendo
16
17
  self.playing_days: int = None
17
18
  self.shop_url: str = None
18
19
  self.name: str = None
@@ -20,11 +21,10 @@ class Application:
20
21
 
21
22
  def update_today_time_played(self, daily_summary: dict):
22
23
  """Updates the today time played for the given application."""
23
- _LOGGER.debug("Updating today time played for app %s",
24
- self.application_id)
24
+ _LOGGER.debug("Updating today time played for app %s", self.application_id)
25
25
  self.today_time_played = daily_summary.get("playingTime", 0)
26
26
 
27
- def update(self, updated: 'Application'):
27
+ def update(self, updated: "Application"):
28
28
  """Updates self with a given application."""
29
29
  _LOGGER.debug("Updating application %s", self.application_id)
30
30
  self.application_id = updated.application_id
@@ -37,7 +37,7 @@ class Application:
37
37
  self.today_time_played = updated.today_time_played
38
38
 
39
39
  @classmethod
40
- def from_daily_summary(cls, raw: list) -> list['Application']:
40
+ def from_daily_summary(cls, raw: list) -> list["Application"]:
41
41
  """Converts a raw daily summary response into a list of applications."""
42
42
  built = []
43
43
  if "playedApps" in raw:
@@ -49,7 +49,7 @@ class Application:
49
49
  return built
50
50
 
51
51
  @staticmethod
52
- def check_if_app_in_list(app_list: list['Application'], app: 'Application') -> bool:
52
+ def check_if_app_in_list(app_list: list["Application"], app: "Application") -> bool:
53
53
  """Checks if an app is in a list."""
54
54
  for app_li in app_list:
55
55
  if app_li.application_id == app.application_id:
@@ -57,7 +57,9 @@ class Application:
57
57
  return False
58
58
 
59
59
  @staticmethod
60
- def return_app_from_list(app_list: list['Application'], application_id: str) -> 'Application':
60
+ def return_app_from_list(
61
+ app_list: list["Application"], application_id: str
62
+ ) -> "Application":
61
63
  """Returns a single app from a given list."""
62
64
  for app in app_list:
63
65
  if app.application_id == application_id:
@@ -65,28 +67,32 @@ class Application:
65
67
  return None
66
68
 
67
69
  @classmethod
68
- def from_whitelist(cls, raw: dict) -> list['Application']:
70
+ def from_whitelist(cls, raw: dict) -> list["Application"]:
69
71
  """Converts a raw whitelist response into a list of applications."""
70
72
  parsed = []
71
73
  for app_id in raw:
72
74
  _LOGGER.debug("Parsing app %s", app_id)
73
75
  internal = cls()
74
76
  internal.application_id = raw[app_id]["applicationId"]
75
- internal.first_played_date = datetime.strptime(raw[app_id]["firstPlayDate"], "%Y-%m-%d")
77
+ internal.first_played_date = datetime.strptime(
78
+ raw[app_id]["firstPlayDate"], "%Y-%m-%d"
79
+ )
76
80
  internal.image_url = raw[app_id]["imageUri"]
77
81
  internal.name = raw[app_id]["title"]
78
82
  parsed.append(internal)
79
83
  return parsed
80
84
 
81
85
  @classmethod
82
- def from_monthly_summary(cls, raw: list) -> list['Application']:
86
+ def from_monthly_summary(cls, raw: list) -> list["Application"]:
83
87
  """Converts a raw monthly summary response into a list of applications."""
84
88
  parsed = []
85
89
  for app in raw:
86
90
  _LOGGER.debug("Parsing app %s", app)
87
91
  internal = cls()
88
92
  internal.application_id = app.get("applicationId").capitalize()
89
- internal.first_played_date = datetime.strptime(app.get("firstPlayDate"), "%Y-%m-%d")
93
+ internal.first_played_date = datetime.strptime(
94
+ app.get("firstPlayDate"), "%Y-%m-%d"
95
+ )
90
96
  internal.has_ugc = app.get("hasUgc", False)
91
97
  internal.image_url = app.get("imageUri").get("small")
92
98
  internal.playing_days = app.get("playingDays", None)
@@ -0,0 +1,18 @@
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
+ """Authentication functions."""
12
+
13
+ def __init__(self, session_token=None, client_session=None):
14
+ super().__init__(
15
+ client_id=CLIENT_ID,
16
+ session_token=session_token,
17
+ client_session=client_session,
18
+ )
@@ -4,6 +4,7 @@
4
4
  import logging
5
5
 
6
6
  _LOGGER = logging.getLogger(__package__)
7
+ CLIENT_ID = "54789befb391a838"
7
8
  MOBILE_APP_PKG = "com.nintendo.znma"
8
9
  MOBILE_APP_VERSION = "2.2.0"
9
10
  MOBILE_APP_BUILD = "560"
@@ -14,47 +15,55 @@ DEVICE_MODEL = "Pixel 4 XL"
14
15
  BASE_URL = "https://app.lp1.znma.srv.nintendo.net/v2"
15
16
  USER_AGENT = f"moon_ANDROID/{MOBILE_APP_VERSION} ({MOBILE_APP_PKG}; build:{MOBILE_APP_BUILD}; {OS_STR})"
16
17
 
17
- DAYS_OF_WEEK = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
18
+ DAYS_OF_WEEK = [
19
+ "monday",
20
+ "tuesday",
21
+ "wednesday",
22
+ "thursday",
23
+ "friday",
24
+ "saturday",
25
+ "sunday",
26
+ ]
18
27
 
19
28
  ENDPOINTS = {
20
29
  "get_account_devices": {
21
30
  "url": "{BASE_URL}/actions/user/fetchOwnedDevices",
22
- "method": "GET"
31
+ "method": "GET",
23
32
  },
24
33
  "get_account_device": {
25
34
  "url": "{BASE_URL}/actions/user/fetchOwnedDevice?deviceId={DEVICE_ID}",
26
- "method": "GET"
35
+ "method": "GET",
27
36
  },
28
37
  "get_device_daily_summaries": {
29
38
  "url": "{BASE_URL}/actions/playSummary/fetchDailySummaries?deviceId={DEVICE_ID}",
30
- "method": "GET"
39
+ "method": "GET",
31
40
  },
32
41
  "get_device_monthly_summaries": {
33
42
  "url": "{BASE_URL}/actions/playSummary/fetchLatestMonthlySummary?deviceId={DEVICE_ID}",
34
- "method": "GET"
43
+ "method": "GET",
35
44
  },
36
45
  "get_device_parental_control_setting": {
37
46
  "url": "{BASE_URL}/actions/parentalControlSetting/fetchParentalControlSetting?deviceId={DEVICE_ID}",
38
- "method": "GET"
47
+ "method": "GET",
39
48
  },
40
49
  "update_restriction_level": {
41
50
  "url": "{BASE_URL}/actions/parentalControlSetting/updateRestrictionLevel",
42
- "method": "POST"
51
+ "method": "POST",
43
52
  },
44
53
  "update_play_timer": {
45
54
  "url": "{BASE_URL}/actions/parentalControlSetting/updatePlayTimer",
46
- "method": "POST"
55
+ "method": "POST",
47
56
  },
48
57
  "update_unlock_code": {
49
58
  "url": "{BASE_URL}/actions/parentalControlSetting/updateUnlockCode",
50
- "method": "POST"
59
+ "method": "POST",
51
60
  },
52
61
  "get_device_monthly_summary": {
53
62
  "url": "{BASE_URL}/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false",
54
- "method": "GET"
63
+ "method": "GET",
55
64
  },
56
65
  "update_extra_playing_time": {
57
66
  "url": "{BASE_URL}/actions/device/updateExtraPlayingTime",
58
- "method": "POST"
59
- }
67
+ "method": "POST",
68
+ },
60
69
  }