pynintendoparental 1.1.3__py3-none-any.whl → 2.1.0__py3-none-any.whl

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.
@@ -2,37 +2,21 @@
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
 
11
- class HttpException(Exception):
12
- """A HTTP error occured"""
13
- def __init__(self, status_code: int, message: str, error_code: str | None = None) -> None:
14
- """Initialize the exception."""
15
- super().__init__(message)
16
- self.status_code = status_code
17
- self.message = message
18
- self.error_code = error_code
19
-
20
- def __str__(self) -> str:
21
- if self.error_code:
22
- return f"HTTP {self.status_code}: {self.message} ({self.error_code})"
23
- return f"HTTP {self.status_code}: {self.message}"
24
-
25
- class InvalidSessionTokenException(HttpException):
26
- """Provided session token was invalid (invalid_grant)."""
27
-
28
- class InvalidOAuthConfigurationException(HttpException):
29
- """The OAuth scopes are invalid."""
30
12
 
31
13
  class NoDevicesFoundException(Exception):
32
14
  """No devices were found for the account."""
33
15
 
16
+
34
17
  class InputValidationError(Exception):
35
18
  """Input Validation Failed."""
19
+
36
20
  value: object
37
21
  error_key: str
38
22
 
@@ -40,11 +24,13 @@ class InputValidationError(Exception):
40
24
  super().__init__(f"{self.__doc__} Received value: {value}")
41
25
  self.value = value
42
26
 
27
+
43
28
  class BedtimeOutOfRangeError(InputValidationError):
44
29
  """Bedtime is outside of the allowed range."""
45
30
 
46
31
  error_key = RangeErrorKeys.BEDTIME
47
32
 
33
+
48
34
  class DailyPlaytimeOutOfRangeError(InputValidationError):
49
35
  """Daily playtime is outside of the allowed range."""
50
36
 
@@ -2,30 +2,32 @@
2
2
 
3
3
  from .const import _LOGGER
4
4
 
5
+
5
6
  class Player:
6
7
  """Defines a single player on a Nintendo device."""
8
+
7
9
  def __init__(self):
8
10
  """Init a player."""
9
- self.player_image: str = None
10
- self.nickname: str = None
11
+ self.player_image: str | None = None
12
+ self.nickname: str | None = None
11
13
  self.apps: list = []
12
- self.player_id: str = None
13
- self.playing_time: int = None
14
+ self.month_summary: dict = {}
15
+ self.player_id: str | None = None
16
+ self.playing_time: int = 0
14
17
 
15
18
  def update_from_daily_summary(self, raw: list[dict]):
16
19
  """Update the current instance of the player from the daily summery"""
17
20
  _LOGGER.debug("Updating player %s daily summary", self.player_id)
18
- for player in raw[0].get("devicePlayers", []):
19
- if self.player_id is player["profile"].get("playerId"):
20
- self.player_id = player["profile"].get("playerId")
21
+ for player in raw[0].get("players", []):
22
+ if self.player_id == player["profile"].get("playerId"):
21
23
  self.player_image = player["profile"].get("imageUri")
22
24
  self.nickname = player["profile"].get("nickname")
23
25
  self.playing_time = player.get("playingTime")
24
- self.apps = player.get("playedApps")
26
+ self.apps = player.get("playedGames")
25
27
  break
26
28
 
27
29
  @classmethod
28
- def from_device_daily_summary(cls, raw: list[dict]) -> list['Player']:
30
+ def from_device_daily_summary(cls, raw: list[dict]) -> list["Player"]:
29
31
  """Converts a daily summary response into a list of players."""
30
32
  players = []
31
33
  _LOGGER.debug("Building players from device daily summary.")
@@ -39,3 +41,12 @@ class Player:
39
41
  players.append(parsed)
40
42
  _LOGGER.debug("Built player %s", parsed.player_id)
41
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import inspect
4
4
 
5
+
5
6
  def is_awaitable(func):
6
7
  """Check if a function is awaitable or not."""
7
8
  return inspect.iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
@@ -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
 
@@ -0,0 +1,17 @@
1
+ pynintendoparental/__init__.py,sha256=gO3rH9gukrFoACVkdqj_liqcgtHxSZlitE0PrVQZGok,2334
2
+ pynintendoparental/_version.py,sha256=Xybt2skBZamGMNlLuOX1IG-h4uIxqUDGAO8MIGWrJac,22
3
+ pynintendoparental/api.py,sha256=cVL17wmkCA4AHnVAGzMkNA-40t9M9Q7iZYNFWxu-Oa8,6534
4
+ pynintendoparental/application.py,sha256=8zTisF3_COgIzKCLIgLpYdHcX4OiwRQU5NH2WUngvxc,3995
5
+ pynintendoparental/authenticator.py,sha256=WPAEAUKmIymIiQUXILYt4B2_3UgYRKEVi6btJRwzjmM,430
6
+ pynintendoparental/const.py,sha256=2fq0VmqFetwLq1YBXatlb7tFaDeR0pnsdja4MLFdbDA,2174
7
+ pynintendoparental/device.py,sha256=CiW-SXYGvjsZzW10Sf1Xu0PA6hkduabpWyWPIxgzKng,25541
8
+ pynintendoparental/enum.py,sha256=NlkfrZTfiur6vTqqwHxeiM7wPQZ-1Y1PlY4Jh_gq4NQ,605
9
+ pynintendoparental/exceptions.py,sha256=zOGZHlBFfdcBNNWHA7t_Ix2Qgd3ON2-SVjmA9dHs578,883
10
+ pynintendoparental/player.py,sha256=Zh3vi0IdOHtn5KW0tT9JigB315ftjQ4uEQylF3D4LWs,2084
11
+ pynintendoparental/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pynintendoparental/utils.py,sha256=gLMibsEOnKUZJgCQKF4Zk517fawZ3mBqMK6MS2g-Um0,199
13
+ pynintendoparental-2.1.0.dist-info/licenses/LICENSE,sha256=zsxHgHVMnyWq121yND8zBl9Rl9H6EF2K9N51B2ZSm_k,1071
14
+ pynintendoparental-2.1.0.dist-info/METADATA,sha256=QRICmit7uYf4V7tR4Eog2JkQcCkJoMBk32UD_xT6MbE,2125
15
+ pynintendoparental-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ pynintendoparental-2.1.0.dist-info/top_level.txt,sha256=QQ5bAl-Ljso16P8KLf1NHrFmKk9jLT7bVJG_rVlIXWk,19
17
+ pynintendoparental-2.1.0.dist-info/RECORD,,
@@ -1,226 +0,0 @@
1
- """Nintendo Authentication."""
2
- from __future__ import annotations
3
-
4
- import logging
5
- import base64
6
- import hashlib
7
- import random
8
- import string
9
-
10
- from urllib.parse import urlencode, urlparse
11
-
12
- from datetime import datetime, timedelta
13
-
14
- import aiohttp
15
-
16
- from pynintendoparental.exceptions import (
17
- HttpException,
18
- InvalidOAuthConfigurationException,
19
- InvalidSessionTokenException
20
- )
21
- from .const import (
22
- TOKEN_URL,
23
- SESSION_TOKEN_URL,
24
- CLIENT_ID,
25
- GRANT_TYPE,
26
- MY_ACCOUNT_ENDPOINT,
27
- REDIRECT_URI,
28
- SCOPES,
29
- AUTHORIZE_URL
30
- )
31
-
32
- _LOGGER = logging.getLogger(__name__)
33
-
34
- def _parse_response_token(token: str) -> dict:
35
- """Parses a response token."""
36
- _LOGGER.debug(">> Parsing response token.")
37
- try:
38
- url = urlparse(token)
39
- params = url.fragment.split('&')
40
- response = {}
41
- for param in params:
42
- response = {
43
- **response,
44
- param.split('=')[0]: param.split('=')[1]
45
- }
46
- return response
47
- except Exception as exc:
48
- raise ValueError("Invalid token provided.") from exc
49
-
50
- def _hash(text: str):
51
- """Hash given text for login."""
52
- text = hashlib.sha256(text.encode()).digest()
53
- text = base64.urlsafe_b64encode(text).decode()
54
- return text.replace("=", "")
55
-
56
- def _rand():
57
- return ''.join(random.choice(string.ascii_letters) for _ in range(50))
58
-
59
- class Authenticator:
60
- """Authentication functions."""
61
-
62
- def __init__(
63
- self,
64
- session_token = None,
65
- auth_code_verifier = None,
66
- client_session: aiohttp.ClientSession = None
67
- ):
68
- """Basic init."""
69
- _LOGGER.debug(">> Init authenticator.")
70
- self._at_expiry: datetime = None
71
- self._access_token: str = None
72
- self.available_scopes: dict = None
73
- self.account_id: str = None
74
- self.account: dict = None
75
- self._auth_code_verifier: str = auth_code_verifier
76
- self._refresh_token: str = None
77
- self._id_token: str = None
78
- self._session_token: str = session_token
79
- self.login_url: str = None
80
- if client_session is None:
81
- client_session = aiohttp.ClientSession()
82
- self.client_session: aiohttp.ClientSession = client_session
83
-
84
- @property
85
- def get_session_token(self) -> str:
86
- """Return the session token."""
87
- return self._session_token
88
-
89
- @property
90
- def access_token(self) -> str:
91
- """Return the formatted access token."""
92
- return f"Bearer {self._id_token}" # v2 seems to use ID token for API access?
93
-
94
- @property
95
- def access_token_expired(self) -> bool:
96
- """Check if the access token has expired."""
97
- return self._at_expiry < (datetime.now()+timedelta(minutes=1))
98
-
99
- async def _request_handler(self, method, url, json=None, data=None, headers: dict=None):
100
- """Send a HTTP request"""
101
- if headers is None:
102
- headers = {}
103
- response: dict = {
104
- "status": 0,
105
- "text": "",
106
- "json": "",
107
- "headers": ""
108
- }
109
- async with self.client_session.request(
110
- method=method,
111
- url=url,
112
- json=json,
113
- data=data,
114
- headers=headers
115
- ) as resp:
116
- response["status"] = resp.status
117
- response["text"] = await resp.text()
118
- response["json"] = await resp.json()
119
- response["headers"] = resp.headers
120
- return response
121
-
122
- def _read_tokens(self, tokens: dict):
123
- """Reads tokens into self."""
124
- self.available_scopes = tokens.get("scope")
125
- self._at_expiry = datetime.now() + timedelta(seconds=tokens.get("expires_in"))
126
- self._id_token = tokens.get("id_token")
127
- self._access_token = tokens.get("access_token")
128
-
129
- async def perform_login(self, session_token_code):
130
- """Retrieves initial tokens."""
131
- _LOGGER.debug("Performing initial login.")
132
- session_token_form = aiohttp.FormData()
133
- session_token_form.add_field("client_id", CLIENT_ID)
134
- session_token_form.add_field("session_token_code", session_token_code)
135
- session_token_form.add_field("session_token_code_verifier", self._auth_code_verifier)
136
- session_token_response = await self._request_handler(
137
- method="POST",
138
- url=SESSION_TOKEN_URL,
139
- data=session_token_form
140
- )
141
-
142
- if session_token_response.get("status") != 200:
143
- raise HttpException(session_token_response.get("status"),
144
- session_token_response.get("text"))
145
-
146
- self._session_token = session_token_response["json"]["session_token"]
147
-
148
- async def perform_refresh(self):
149
- """Refresh the access token."""
150
- _LOGGER.debug("Refreshing access token.")
151
- token_response = await self._request_handler(
152
- method="POST",
153
- url=TOKEN_URL,
154
- json={
155
- "client_id": CLIENT_ID,
156
- "grant_type": GRANT_TYPE,
157
- "session_token": self.get_session_token
158
- }
159
- )
160
-
161
- if token_response["status"] == 400:
162
- raise InvalidSessionTokenException(400, token_response["json"]["error"])
163
-
164
- if token_response["status"] == 401:
165
- raise InvalidOAuthConfigurationException(401, token_response["json"]["error"])
166
-
167
- if token_response.get("status") != 200:
168
- raise HttpException(token_response.get("status"), f"login error {token_response.get('status')}")
169
-
170
- self._read_tokens(token_response.get("json"))
171
- if self.account_id is None:
172
- # fill account_id
173
- account = await self._request_handler(
174
- method="GET",
175
- url=MY_ACCOUNT_ENDPOINT,
176
- headers={
177
- "Authorization": f"Bearer {self._access_token}"
178
- }
179
- )
180
- if account["status"] != 200:
181
- raise HttpException(account["status"], f"Unable to get account_id {account['status']}")
182
- self.account_id = account["json"]["id"]
183
- self.account = account["json"]
184
-
185
- @classmethod
186
- def generate_login(
187
- cls,
188
- client_session: aiohttp.ClientSession | None = None) -> 'Authenticator':
189
- """Starts configuration of the authenticator."""
190
- verifier = _rand()
191
-
192
- auth = cls(auth_code_verifier=verifier, client_session=client_session)
193
-
194
- query = {
195
- "client_id": CLIENT_ID,
196
- # "interacted": 1,
197
- "redirect_uri": REDIRECT_URI,
198
- "response_type": "session_token_code",
199
- "scope": "+".join(SCOPES),
200
- "session_token_code_challenge": _hash(verifier),
201
- "session_token_code_challenge_method": "S256",
202
- "state": _rand(),
203
- "theme": "login_form"
204
- }
205
-
206
- auth.login_url = AUTHORIZE_URL.format(urlencode(query)).replace("%2B", "+")
207
- return auth
208
-
209
- @classmethod
210
- async def complete_login(cls,
211
- auth: Authenticator | None,
212
- response_token: str,
213
- is_session_token: bool=False,
214
- client_session: aiohttp.ClientSession | None = None) -> Authenticator:
215
- """Creates and logs into Nintendo APIs"""
216
- if is_session_token:
217
- auth = cls(session_token=response_token, client_session=client_session)
218
- await auth.perform_refresh()
219
- else:
220
- response_token = _parse_response_token(response_token)
221
- await auth.perform_login(
222
- session_token_code=response_token.get("session_token_code")
223
- )
224
- await auth.perform_refresh()
225
-
226
- return auth
@@ -1,29 +0,0 @@
1
- # pylint: disable=line-too-long
2
- """Static values."""
3
-
4
- CLIENT_ID = "54789befb391a838"
5
- GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer-session-token"
6
-
7
- REDIRECT_URI = f"npf{CLIENT_ID}://auth"
8
- SCOPES = [
9
- "openid",
10
- "user",
11
- "user.mii",
12
- "moonUser:administration",
13
- "moonDevice:create",
14
- "moonOwnedDevice:administration",
15
- "moonParentalControlSetting",
16
- "moonParentalControlSetting:update",
17
- "moonParentalControlSettingState",
18
- "moonPairingState",
19
- "moonSmartDevice:administration",
20
- "moonDailySummary",
21
- "moonMonthlySummary",
22
- ]
23
-
24
- AUTHORIZE_URL = "https://accounts.nintendo.com/connect/1.0.0/authorize?{}"
25
- SESSION_TOKEN_URL = "https://accounts.nintendo.com/connect/1.0.0/api/session_token"
26
- TOKEN_URL = "https://accounts.nintendo.com/connect/1.0.0/api/token"
27
-
28
- ACCOUNT_API_BASE = "https://api.accounts.nintendo.com/2.0.0"
29
- MY_ACCOUNT_ENDPOINT = f"{ACCOUNT_API_BASE}/users/me"
@@ -1,18 +0,0 @@
1
- pynintendoparental/__init__.py,sha256=pNcBsHRa4B85USP7uzwPEGF9fu3MA9YgW_hI82F_NXQ,2460
2
- pynintendoparental/_version.py,sha256=u9ExJqoMv3fQc8WmLTw4I2FnQo6u4xRrBc6DLy6G1IE,22
3
- pynintendoparental/api.py,sha256=0rqfgSE3mYemk9-220zpdA6Q0HIs_Xyst3WyLOjZqDw,7502
4
- pynintendoparental/application.py,sha256=l-oVwM4hrVVUf_2djQ7rJVya7LQP38yhaLPAWt8V8TY,3941
5
- pynintendoparental/const.py,sha256=sQZqU0f1NSClMPfCSJonlCunLdbPPiXjL-JS2LMZGd4,2101
6
- pynintendoparental/device.py,sha256=VhWVE3IRbxKMwKnAHgMdGmXT-eZFsbCfYE2VFRIvDek,23223
7
- pynintendoparental/enum.py,sha256=whmcX5JRx60tq6OgviId_0c5JZpiBvmPZSnY6UTGtb8,599
8
- pynintendoparental/exceptions.py,sha256=rf8uroPkRB9zcIpjQCzKoXJqOzMOAQyy6KITs1vLmoQ,1628
9
- pynintendoparental/player.py,sha256=WDl0pspHgrV9lGhDp-NKlfP8DV4Yxe02aYaGg9wTTeg,1785
10
- pynintendoparental/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- pynintendoparental/utils.py,sha256=5-EP_rmPnSSWtbi18Y226GtjLhF3PLONKwmRdiy7m2c,198
12
- pynintendoparental/authenticator/__init__.py,sha256=MZdA6qqHV0i7rspNL9Z9xl7aRy-EJEm3NIapiIgEJBA,7688
13
- pynintendoparental/authenticator/const.py,sha256=_nUJVC0U64j_n1LaQd_KDg0EWFcezb87bQyYYXpbPPY,917
14
- pynintendoparental-1.1.3.dist-info/licenses/LICENSE,sha256=zsxHgHVMnyWq121yND8zBl9Rl9H6EF2K9N51B2ZSm_k,1071
15
- pynintendoparental-1.1.3.dist-info/METADATA,sha256=w1pPcFJx1kiBOpDwzz9es7kuKASf2gFa8ksGEaCBqhM,1871
16
- pynintendoparental-1.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- pynintendoparental-1.1.3.dist-info/top_level.txt,sha256=QQ5bAl-Ljso16P8KLf1NHrFmKk9jLT7bVJG_rVlIXWk,19
18
- pynintendoparental-1.1.3.dist-info/RECORD,,