pynintendoparental 1.1.3__tar.gz → 2.1.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 (31) hide show
  1. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/PKG-INFO +8 -2
  2. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/__init__.py +11 -13
  3. pynintendoparental-2.1.0/pynintendoparental/_version.py +1 -0
  4. pynintendoparental-2.1.0/pynintendoparental/api.py +183 -0
  5. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/application.py +17 -11
  6. pynintendoparental-2.1.0/pynintendoparental/authenticator.py +18 -0
  7. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/const.py +21 -12
  8. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/device.py +219 -109
  9. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/enum.py +6 -0
  10. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/exceptions.py +5 -19
  11. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/player.py +20 -9
  12. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/utils.py +1 -0
  13. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental.egg-info/PKG-INFO +8 -2
  14. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental.egg-info/SOURCES.txt +5 -2
  15. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental.egg-info/requires.txt +6 -1
  16. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/setup.py +6 -1
  17. pynintendoparental-2.1.0/tests/test_api.py +193 -0
  18. pynintendoparental-2.1.0/tests/test_device.py +110 -0
  19. pynintendoparental-2.1.0/tests/test_init.py +80 -0
  20. pynintendoparental-2.1.0/tests/test_player.py +46 -0
  21. pynintendoparental-1.1.3/pynintendoparental/_version.py +0 -1
  22. pynintendoparental-1.1.3/pynintendoparental/api.py +0 -221
  23. pynintendoparental-1.1.3/pynintendoparental/authenticator/__init__.py +0 -226
  24. pynintendoparental-1.1.3/pynintendoparental/authenticator/const.py +0 -29
  25. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/LICENSE +0 -0
  26. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/README.md +0 -0
  27. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental/py.typed +0 -0
  28. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental.egg-info/dependency_links.txt +0 -0
  29. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pynintendoparental.egg-info/top_level.txt +0 -0
  30. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/pyproject.toml +0 -0
  31. {pynintendoparental-1.1.3 → pynintendoparental-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 1.1.3
3
+ Version: 2.1.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,15 +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==1.0.0
14
15
  Provides-Extra: dev
16
+ Requires-Dist: aiofiles<26,>=23; extra == "dev"
15
17
  Requires-Dist: bandit<1.9,>=1.7; extra == "dev"
16
18
  Requires-Dist: black<26,>=23; extra == "dev"
17
19
  Requires-Dist: build<1.4,>=0.10; extra == "dev"
20
+ Requires-Dist: Faker<39,>=38; extra == "dev"
18
21
  Requires-Dist: flake8<8,>=6; extra == "dev"
19
22
  Requires-Dist: isort<7,>=5; extra == "dev"
20
- Requires-Dist: mypy<1.19,>=1.5; extra == "dev"
23
+ Requires-Dist: mypy<1.20,>=1.5; extra == "dev"
21
24
  Requires-Dist: pytest<9,>=7; extra == "dev"
22
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"
23
28
  Requires-Dist: twine<7,>=4; extra == "dev"
24
29
  Dynamic: author
25
30
  Dynamic: classifier
@@ -29,6 +34,7 @@ Dynamic: home-page
29
34
  Dynamic: license
30
35
  Dynamic: license-file
31
36
  Dynamic: provides-extra
37
+ Dynamic: requires-dist
32
38
  Dynamic: requires-python
33
39
  Dynamic: summary
34
40
 
@@ -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.1.0"
@@ -0,0 +1,183 @@
1
+ """API handler."""
2
+
3
+ import aiohttp
4
+
5
+ from pynintendoauth.exceptions import HttpException
6
+
7
+ from .authenticator import Authenticator
8
+ from .const import (
9
+ ENDPOINTS,
10
+ BASE_URL,
11
+ USER_AGENT,
12
+ MOBILE_APP_PKG,
13
+ MOBILE_APP_BUILD,
14
+ MOBILE_APP_VERSION,
15
+ OS_VERSION,
16
+ OS_NAME,
17
+ DEVICE_MODEL,
18
+ _LOGGER,
19
+ )
20
+
21
+
22
+ def _check_http_success(status: int) -> bool:
23
+ return status >= 200 and status < 300
24
+
25
+
26
+ class Api:
27
+ """Nintendo Parental Controls API."""
28
+
29
+ def __init__(self, auth, tz, lang):
30
+ """INIT"""
31
+ self._auth: Authenticator = auth
32
+ self._tz = tz
33
+ self._language = lang
34
+
35
+ @property
36
+ def account_id(self):
37
+ """Return the account id."""
38
+ return self._auth.account_id
39
+
40
+ @property
41
+ def _headers(self) -> dict:
42
+ """Return web request headers."""
43
+ return {
44
+ "User-Agent": USER_AGENT,
45
+ "X-Moon-App-Id": MOBILE_APP_PKG,
46
+ "X-Moon-Os": OS_NAME,
47
+ "X-Moon-Os-Version": OS_VERSION,
48
+ "X-Moon-Model": DEVICE_MODEL,
49
+ "X-Moon-App-Display-Version": MOBILE_APP_VERSION,
50
+ "X-Moon-App-Internal-Version": MOBILE_APP_BUILD,
51
+ "X-Moon-TimeZone": self._tz,
52
+ "X-Moon-Os-Language": self._language,
53
+ "X-Moon-App-Language": self._language,
54
+ "Authorization": self._auth.access_token,
55
+ }
56
+
57
+ async def send_request(self, endpoint: str, body: object = None, **kwargs):
58
+ """Sends a request to a given endpoint."""
59
+ _LOGGER.debug("Sending request to %s", endpoint)
60
+ # Get the endpoint from the endpoints map
61
+ e_point = ENDPOINTS.get(endpoint, None)
62
+ if e_point is None:
63
+ raise ValueError("Endpoint does not exist")
64
+ # refresh the token if it has expired.
65
+ if self._auth.access_token_expired:
66
+ _LOGGER.debug("Access token expired, requesting refresh.")
67
+ await self._auth.perform_refresh()
68
+ # format the URL using the kwargs
69
+ url = e_point.get("url").format(BASE_URL=BASE_URL, **kwargs)
70
+ _LOGGER.debug("Built URL %s", url)
71
+ # now send the HTTP request
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":
84
+ try:
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
111
+
112
+ # now return the resp dict
113
+ return resp
114
+
115
+ async def async_get_account_devices(self) -> dict:
116
+ """Get account devices."""
117
+ return await self.send_request(endpoint="get_account_devices")
118
+
119
+ async def async_get_account_device(self, device_id: str) -> dict:
120
+ """Get account device."""
121
+ return await self.send_request(
122
+ endpoint="get_account_device", DEVICE_ID=device_id
123
+ )
124
+
125
+ async def async_get_device_daily_summaries(self, device_id: str) -> dict:
126
+ """Get device daily summaries."""
127
+ return await self.send_request(
128
+ endpoint="get_device_daily_summaries", DEVICE_ID=device_id
129
+ )
130
+
131
+ async def async_get_device_monthly_summaries(self, device_id: str) -> dict:
132
+ """Get device monthly summaries."""
133
+ return await self.send_request(
134
+ endpoint="get_device_monthly_summaries", DEVICE_ID=device_id
135
+ )
136
+
137
+ async def async_get_device_parental_control_setting(self, device_id: str) -> dict:
138
+ """Get device parental control setting."""
139
+ return await self.send_request(
140
+ endpoint="get_device_parental_control_setting", DEVICE_ID=device_id
141
+ )
142
+
143
+ async def async_get_device_monthly_summary(
144
+ self, device_id: str, year: int, month: int
145
+ ) -> dict:
146
+ """Get device monthly summary."""
147
+ return await self.send_request(
148
+ endpoint="get_device_monthly_summary",
149
+ DEVICE_ID=device_id,
150
+ YEAR=year,
151
+ MONTH=f"{month:02d}",
152
+ )
153
+
154
+ async def async_update_restriction_level(self, settings: dict) -> dict:
155
+ """Update device restriction level."""
156
+ return await self.send_request(
157
+ endpoint="update_restriction_level", body=settings
158
+ )
159
+
160
+ async def async_update_play_timer(self, settings: dict) -> dict:
161
+ """Update device play timer settings."""
162
+ return await self.send_request(endpoint="update_play_timer", body=settings)
163
+
164
+ async def async_update_unlock_code(self, new_code: str, device_id: str) -> dict:
165
+ """Update device unlock code."""
166
+ return await self.send_request(
167
+ endpoint="update_unlock_code",
168
+ body={"deviceId": device_id, "unlockCode": new_code},
169
+ )
170
+
171
+ async def async_update_extra_playing_time(
172
+ self, device_id: str, additional_time: int
173
+ ) -> dict:
174
+ """Update device extra playing time."""
175
+ body = {
176
+ "deviceId": device_id,
177
+ "additionalTime": additional_time,
178
+ "status": "TO_ADDED",
179
+ }
180
+ if additional_time == -1:
181
+ body["status"] = "TO_INFINITY"
182
+ body.pop("additionalTime")
183
+ 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
  }