pynintendoparental 1.1.2__py3-none-any.whl → 2.0.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.
@@ -1,9 +1,11 @@
1
1
  """Enums"""
2
2
 
3
- from enum import Enum
3
+ from enum import Enum, StrEnum
4
+
4
5
 
5
6
  class AlarmSettingState(Enum):
6
7
  """Alarm setting states."""
8
+
7
9
  SUCCESS = 0
8
10
  TO_VISIBLE = 1
9
11
  TO_INVISIBLE = 2
@@ -13,10 +15,22 @@ class AlarmSettingState(Enum):
13
15
  def __str__(self) -> str:
14
16
  return self.name
15
17
 
18
+
16
19
  class RestrictionMode(Enum):
17
20
  """Restriction modes."""
21
+
18
22
  FORCED_TERMINATION = 0
19
23
  ALARM = 1
20
24
 
21
25
  def __str__(self) -> str:
22
26
  return self.name
27
+
28
+
29
+ class DeviceTimerMode(StrEnum):
30
+ """Device timer modes."""
31
+
32
+ DAILY = "DAILY"
33
+ EACH_DAY_OF_THE_WEEK = "EACH_DAY_OF_THE_WEEK"
34
+
35
+ def __str__(self) -> str:
36
+ return self.name
@@ -8,28 +8,13 @@ class RangeErrorKeys(StrEnum):
8
8
  DAILY_PLAYTIME = "daily_playtime_out_of_range"
9
9
  BEDTIME = "bedtime_alarm_out_of_range"
10
10
 
11
- class HttpException(Exception):
12
- """A HTTP error occured"""
13
- def __init__(self, status_code: int, message: str) -> None:
14
- """Initialize the exception."""
15
- super().__init__(message)
16
- self.status_code = status_code
17
- self.message = message
18
-
19
- def __str__(self) -> str:
20
- return f"HTTP {self.status_code}: {self.message}"
21
-
22
- class InvalidSessionTokenException(HttpException):
23
- """Provided session token was invalid (invalid_grant)."""
24
-
25
- class InvalidOAuthConfigurationException(HttpException):
26
- """The OAuth scopes are invalid."""
27
-
28
11
  class NoDevicesFoundException(Exception):
29
12
  """No devices were found for the account."""
30
13
 
14
+
31
15
  class InputValidationError(Exception):
32
16
  """Input Validation Failed."""
17
+
33
18
  value: object
34
19
  error_key: str
35
20
 
@@ -37,11 +22,13 @@ class InputValidationError(Exception):
37
22
  super().__init__(f"{self.__doc__} Received value: {value}")
38
23
  self.value = value
39
24
 
25
+
40
26
  class BedtimeOutOfRangeError(InputValidationError):
41
27
  """Bedtime is outside of the allowed range."""
42
28
 
43
29
  error_key = RangeErrorKeys.BEDTIME
44
30
 
31
+
45
32
  class DailyPlaytimeOutOfRangeError(InputValidationError):
46
33
  """Daily playtime is outside of the allowed range."""
47
34
 
@@ -2,8 +2,10 @@
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
11
  self.player_image: str = None
@@ -25,7 +27,7 @@ class Player:
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.")
@@ -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.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
 
@@ -0,0 +1,17 @@
1
+ pynintendoparental/__init__.py,sha256=gO3rH9gukrFoACVkdqj_liqcgtHxSZlitE0PrVQZGok,2334
2
+ pynintendoparental/_version.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
3
+ pynintendoparental/api.py,sha256=LmZei8WPzEhCEoVLXXHiaKRbDEbmGyD6CyfPS4fXxnc,7047
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=VSK1-fKcXA2AoGCKaVGyhy32T6VXYowuo7upNivaWyk,24521
8
+ pynintendoparental/enum.py,sha256=NlkfrZTfiur6vTqqwHxeiM7wPQZ-1Y1PlY4Jh_gq4NQ,605
9
+ pynintendoparental/exceptions.py,sha256=8a_7ocrlB0PwPM36Q4FqR-W0lkGXihOs79gGqQIWZoo,881
10
+ pynintendoparental/player.py,sha256=rXYgUiy7DhBjosQlADr7Fa_HVdA1e-04oPZiC4Y68oM,1787
11
+ pynintendoparental/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pynintendoparental/utils.py,sha256=gLMibsEOnKUZJgCQKF4Zk517fawZ3mBqMK6MS2g-Um0,199
13
+ pynintendoparental-2.0.0.dist-info/licenses/LICENSE,sha256=zsxHgHVMnyWq121yND8zBl9Rl9H6EF2K9N51B2ZSm_k,1071
14
+ pynintendoparental-2.0.0.dist-info/METADATA,sha256=5LTsNbTAUk98bovyA4h57PuFT6-iC470Hk98ErArp7Q,1931
15
+ pynintendoparental-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ pynintendoparental-2.0.0.dist-info/top_level.txt,sha256=QQ5bAl-Ljso16P8KLf1NHrFmKk9jLT7bVJG_rVlIXWk,19
17
+ pynintendoparental-2.0.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=5SgGjThsHu_ITn8V83BvCziqCwxdXxTQqcC3KQMHPfM,22
3
- pynintendoparental/api.py,sha256=hMXq0eNIgFELlNZJtN0rK3plKyu9nirvwiUPNlkjOCY,7013
4
- pynintendoparental/application.py,sha256=l-oVwM4hrVVUf_2djQ7rJVya7LQP38yhaLPAWt8V8TY,3941
5
- pynintendoparental/const.py,sha256=sQZqU0f1NSClMPfCSJonlCunLdbPPiXjL-JS2LMZGd4,2101
6
- pynintendoparental/device.py,sha256=EEQfnab96CIGlB2n1gePQBn5SWx7cck8tX_qz8xAHjc,22956
7
- pynintendoparental/enum.py,sha256=lzacGti7fcQqAOROjB9782De7bOMYKSEM61SQd6aYG4,401
8
- pynintendoparental/exceptions.py,sha256=-KuRBoMtzWKycjdgIsRYPzSVi_uzPjROq7blJSgq-iw,1450
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.2.dist-info/licenses/LICENSE,sha256=zsxHgHVMnyWq121yND8zBl9Rl9H6EF2K9N51B2ZSm_k,1071
15
- pynintendoparental-1.1.2.dist-info/METADATA,sha256=wV4_2Gwy86YLWZe8SOu5svDLpH2VWsi5ORPCVfYzLKk,1871
16
- pynintendoparental-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- pynintendoparental-1.1.2.dist-info/top_level.txt,sha256=QQ5bAl-Ljso16P8KLf1NHrFmKk9jLT7bVJG_rVlIXWk,19
18
- pynintendoparental-1.1.2.dist-info/RECORD,,