Repository: pantherale0/pynintendoparental
Pull request #29: Refactor Device class API calls to async methods
Author: login: pantherale0
Labels: name: enhancement
description: New feature or request
Created: 2025-05-21T15:43:02Z | Merged: 2025-05-21T18:22:09Z

Improve performance by refactoring API calls in the Device class to utilize asynchronous methods. This change enhances the efficiency of device-related operations.


Diff:
diff --git a/pynintendoparental/__init__.py b/pynintendoparental/__init__.py
index f72f5d2..0386b6e 100644
--- a/pynintendoparental/__init__.py
+++ b/pynintendoparental/__init__.py
@@ -30,10 +30,7 @@ async def update_device(dev: Device):
                               dev.device_id,
                               err)
 
-        response = await self._api.send_request(
-            endpoint="get_account_devices",
-            ACCOUNT_ID=self.account_id
-        )
+        response = await self._api.async_get_account_devices()
 
         for dev_raw in response["json"]["items"]:
             device: Device = Device.from_device_response(dev_raw, self._api)
diff --git a/pynintendoparental/api.py b/pynintendoparental/api.py
index b2647c7..4ad360c 100644
--- a/pynintendoparental/api.py
+++ b/pynintendoparental/api.py
@@ -25,11 +25,15 @@ def _check_http_success(status: int) -> bool:
 class Api:
     """Nintendo Parental Controls API."""
 
-    def __init__(self, auth, tz, lang):
+    def __init__(self, auth, tz, lang, session: aiohttp.ClientSession=None):
         """INIT"""
         self._auth: Authenticator = auth
         self._tz = tz
         self._language = lang
+        if session is None:
+            session = aiohttp.ClientSession()
+            self._session_created_internally = True
+        self._session = session
 
     @property
     def _auth_token(self) -> str:
@@ -41,6 +45,29 @@ def account_id(self):
         """Return the account id."""
         return self._auth.account_id
 
+    @property
+    def _headers(self) -> dict:
+        """Return web request headers."""
+        return {
+            "User-Agent": USER_AGENT,
+            "X-Moon-App-Id": MOBILE_APP_PKG,
+            "X-Moon-Os": OS_NAME,
+            "X-Moon-Os-Version": OS_VERSION,
+            "X-Moon-Model": DEVICE_MODEL,
+            "X-Moon-App-Display-Version": MOBILE_APP_VERSION,
+            "X-Moon-App-Internal-Version": MOBILE_APP_BUILD,
+            "X-Moon-TimeZone": self._tz,
+            "X-Moon-Os-Language": self._language,
+            "X-Moon-App-Language": self._language,
+            "Authorization": self._auth_token
+        }
+
+    async def async_close(self):
+        """Closes the underlying aiohttp.ClientSession if it was created by this instance."""
+        if hasattr(self, '_session_created_internally') and self._session_created_internally and self._session and not self._session.closed:
+            await self._session.close()
+            self._session = None # Optional: clear the session attribute
+
     async def send_request(self, endpoint: str, body: object=None, **kwargs):
         """Sends a request to a given endpoint."""
         _LOGGER.debug("Sending request to %s", endpoint)
@@ -62,35 +89,134 @@ async def send_request(self, endpoint: str, body: object=None, **kwargs):
             "json": "",
             "headers": ""
         }
-        async with aiohttp.ClientSession() as session:
-            # Add auth header
-            session.headers.add("Authorization", self._auth_token)
-            session.headers.add("User-Agent", USER_AGENT)
-            session.headers.add("X-Moon-App-Id", MOBILE_APP_PKG)
-            session.headers.add("X-Moon-Os", OS_NAME)
-            session.headers.add("X-Moon-Os-Version", OS_VERSION)
-            session.headers.add("X-Moon-Model", DEVICE_MODEL)
-            session.headers.add("X-Moon-TimeZone", self._tz)
-            session.headers.add("X-Moon-Os-Language", self._language)
-            session.headers.add("X-Moon-App-Language", self._language)
-            session.headers.add("X-Moon-App-Display-Version", MOBILE_APP_VERSION)
-            session.headers.add("X-Moon-App-Internal-Version", MOBILE_APP_BUILD)
-            async with session.request(
-                method=e_point.get("method"),
-                url=url,
-                json=body
-            ) as response:
-                _LOGGER.debug("%s request to %s status code %s",
-                              e_point.get("method"),
-                              url,
-                              response.status)
-                if _check_http_success(response.status):
-                    resp["status"] = response.status
-                    resp["text"] = await response.text()
+        self._session.headers.update(self._headers)
+        async with self._session.request(
+            method=e_point.get("method"),
+            url=url,
+            json=body
+        ) as response:
+            _LOGGER.debug("%s request to %s status code %s",
+                            e_point.get("method"),
+                            url,
+                            response.status)
+            if _check_http_success(response.status):
+                resp["status"] = response.status
+                resp["text"] = await response.text()
+                try:
                     resp["json"] = await response.json()
-                    resp["headers"] = response.headers
-                else:
-                    raise HttpException("HTTP Error", response.status, await response.text())
+                except (aiohttp.ContentTypeError, ValueError) as e:
+                    _LOGGER.warning(
+                        """Failed to decode JSON response from %s.
+                        Status: %s, Error: %s.
+                        Response text: %s...""",
+                        url, response.status, e, resp['text'][:200]
+                    )
+                    resp["json"] = {}
+                resp["headers"] = response.headers
+            else:
+                raise HttpException("HTTP Error", response.status, await response.text())
 
         # now return the resp dict
         return resp
+
+    async def async_get_account_details(self) -> dict:
+        """Get account details."""
+        return await self.send_request(
+            endpoint="get_account_details",
+            ACCOUNT_ID=self.account_id
+        )
+
+    async def async_get_account_devices(self) -> dict:
+        """Get account devices."""
+        return await self.send_request(
+            endpoint="get_account_devices",
+            ACCOUNT_ID=self.account_id
+        )
+
+    async def async_get_account_device(self, device_id: str) -> dict:
+        """Get account device."""
+        return await self.send_request(
+            endpoint="get_account_device",
+            ACCOUNT_ID=self.account_id,
+            DEVICE_ID=device_id
+        )
+
+    async def async_get_device_daily_summaries(self, device_id: str) -> dict:
+        """Get device daily summaries."""
+        return await self.send_request(
+            endpoint="get_device_daily_summaries",
+            DEVICE_ID=device_id
+        )
+
+    async def async_get_device_monthly_summaries(self, device_id: str) -> dict:
+        """Get device monthly summaries."""
+        return await self.send_request(
+            endpoint="get_device_monthly_summaries",
+            DEVICE_ID=device_id
+        )
+
+    async def async_get_device_parental_control_setting(self, device_id: str) -> dict:
+        """Get device parental control setting."""
+        return await self.send_request(
+            endpoint="get_device_parental_control_setting",
+            DEVICE_ID=device_id
+        )
+
+    async def async_get_device_parental_control_setting_state(self, device_id: str) -> dict:
+        """Get device parental control setting state."""
+        return await self.send_request(
+            endpoint="get_device_parental_control_setting_state",
+            DEVICE_ID=device_id
+        )
+
+    async def async_get_device_alarm_setting_state(self, device_id: str) -> dict:
+        """Get device alarm setting state."""
+        return await self.send_request(
+            endpoint="get_device_alarm_setting_state",
+            DEVICE_ID=device_id
+        )
+
+    async def async_get_device_monthly_summary(self, device_id: str, year: int, month: int) -> dict:
+        """Get device monthly summary."""
+        return await self.send_request(
+            endpoint="get_device_monthly_summary",
+            DEVICE_ID=device_id,
+            YEAR=year,
+            MONTH=f"{month:02d}"
+        )
+
+    async def async_set_device_parental_control_setting(
+            self,
+            device_id: str,
+            settings: dict
+        ) -> dict:
+        """Update device parental control setting."""
+        return await self.send_request(
+            endpoint="update_device_parental_control_setting",
+            DEVICE_ID=device_id,
+            body=settings
+        )
+
+    async def async_set_device_whitelisted_applications(
+            self,
+            device_id: str,
+            applications: dict
+        ) -> dict:
+        """Update device whitelisted applications."""
+        return await self.send_request(
+            endpoint="update_device_whitelisted_applications",
+            DEVICE_ID=device_id,
+            body=applications
+        )
+
+    async def async_set_device_alarm_setting_state(
+            self,
+            device_id: str,
+            alarm_state: dict
+        ) -> dict:
+        """Update device alarm setting state."""
+        return await self.send_request(
+            endpoint="update_device_alarm_setting_state",
+            DEVICE_ID=device_id,
+            body=alarm_state
+        )
diff --git a/pynintendoparental/device.py b/pynintendoparental/device.py
index 745f506..b3611c7 100644
--- a/pynintendoparental/device.py
+++ b/pynintendoparental/device.py
@@ -115,10 +115,9 @@ async def set_bedtime_alarm(self, end_time: time = None, enabled: bool = True):
     async def _set_parental_control_setting(self):
         """Shortcut method to deduplicate code used to update parental control settings."""
         _LOGGER.debug(">> Device._set_parental_control_setting()")
-        await self._api.send_request(
-            endpoint="update_device_parental_control_setting",
-            body=self._get_update_parental_control_setting_body(),
-            DEVICE_ID=self.device_id
+        await self._api.async_set_device_parental_control_setting(
+            settings=self._get_update_parental_control_setting_body(),
+            device_id=self.device_id
         )
         await self._get_parental_control_setting()
 
@@ -209,9 +208,8 @@ def _update_day_of_week_regulations(self):
     async def _get_parental_control_setting(self):
         """Retreives parental control settings from the API."""
         _LOGGER.debug(">> Device._get_parental_control_setting()")
-        response = await self._api.send_request(
-            endpoint="get_device_parental_control_setting",
-            DEVICE_ID=self.device_id
+        response = await self._api.async_get_device_parental_control_setting(
+            device_id=self.device_id
         )
         self.parental_control_settings = response["json"]
         if "bedtimeStartingTime" in self.parental_control_settings["playTimerRegulations"]:
@@ -235,9 +233,8 @@ def _get_whitelisted_applications(self):
     async def _get_daily_summaries(self):
         """Retrieve daily summaries."""
         _LOGGER.debug(">> Device._get_daily_summaries()")
-        response = await self._api.send_request(
-            endpoint="get_device_daily_summaries",
-            DEVICE_ID = self.device_id
+        response = await self._api.async_get_device_daily_summaries(
+            device_id = self.device_id
         )
         self.daily_summaries = response["json"]["items"]
         _LOGGER.debug("New daily summary %s", self.daily_summaries)
@@ -334,10 +331,8 @@ async def _get_extras(self):
         _LOGGER.debug(">> Device._get_extras()")
         if self.alarms_enabled is not None:
             # first refresh can come from self.extra without http request
-            response = await self._api.send_request(
-                endpoint="get_account_device",
-                ACCOUNT_ID = self._api.account_id,
-                DEVICE_ID = self.device_id
+            response = await self._api.async_get_account_device(
+                device_id = self.device_id
             )
             self.extra = response["json"]
         status = self.extra["device"]["alarmSetting"]["visibility"]
@@ -351,9 +346,8 @@ async def get_monthly_summary(self, search_date: datetime = None):
         _LOGGER.debug(">> Device.get_monthly_summary(search_date=%s)", search_date)
         latest = False
         if search_date is None:
-            response = await self._api.send_request(
-                endpoint="get_device_monthly_summaries",
-                DEVICE_ID=self.device_id
+            response = await self._api.async_get_device_monthly_summaries(
+                device_id=self.device_id
             )
             _LOGGER.debug("Available monthly summaries: %s", response)
             response = response["json"]["indexes"][0]
@@ -362,11 +356,10 @@ async def get_monthly_summary(self, search_date: datetime = None):
             latest = True
 
         try:
-            response = await self._api.send_request(
-                endpoint="get_device_monthly_summary",
-                DEVICE_ID = self.device_id,
-                YEAR=search_date.year,
-                MONTH=str(search_date.month).zfill(2)
+            response = await self._api.async_get_device_monthly_summary(
+                device_id = self.device_id,
+                year=search_date.year,
+                month=search_date.month
             )
             _LOGGER.debug("Monthly summary query complete for device %s: %s",
                         self.device_id,
@@ -385,12 +378,11 @@ async def set_alarm_state(self, state: AlarmSettingState):
         """Updates the alarm state for the device."""
         _LOGGER.debug(">> Device.set_alarm_state(state=%s)",
                       state)
-        await self._api.send_request(
-            endpoint="update_device_alarm_setting_state",
-            body={
+        await self._api.async_set_device_alarm_setting_state(
+            alarm_state={
                 "status": str(state)
             },
-            DEVICE_ID = self.device_id
+            device_id = self.device_id
         )
 
     async def set_whitelisted_application(self, app_id: str, allowed: bool):
@@ -403,10 +395,9 @@ async def set_whitelisted_application(self, app_id: str, allowed: bool):
         # take a snapshot of the whitelisted apps state
         current_state = self.parental_control_settings["whitelistedApplications"]
         current_state[app_id]["safeLaunch"] = "ALLOW" if allowed else "NONE"
-        await self._api.send_request(
-            endpoint="update_device_whitelisted_applications",
-            body=current_state,
-            DEVICE_ID=self.device_id
+        await self._api.async_set_device_whitelisted_applications(
+            applications=current_state,
+            device_id=self.device_id
         )
         await self._get_parental_control_setting()
 


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @pantherale0, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
Hello! Gemini or gemini-code-assist here, providing a summary of this pull request. This PR aims to improve the performance and efficiency of the `pynintendoparental` library by refactoring the API calls within the `Device` class to use asynchronous methods. This involves introducing dedicated async methods in the `Api` class for specific endpoints and modifying the `Device` class to call these new methods instead of the generic `send_request`.

### Highlights
* **Asynchronous API Calls**: The core change is the introduction of dedicated asynchronous methods in the `Api` class (e.g., `async_get_account_devices`, `async_set_device_parental_control_setting`) to wrap calls to the generic `send_request` method. This provides a clearer, more specific API for interacting with the Nintendo Parental Controls service.
* **Improved Session Management**: The `Api` class constructor now accepts an `aiohttp.ClientSession` object. The `send_request` method has been updated to use this shared session (`self._session`) instead of creating a new session for every request. This is a standard practice in asynchronous programming with `aiohttp` to improve performance by reusing connections.
* **Refactored Device Class**: The `Device` class methods that previously called `self._api.send_request` directly have been updated to call the new, specific asynchronous methods introduced in the `Api` class. This makes the `Device` class code cleaner and more readable, relying on the `Api` class to handle the details of endpoint mapping.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **pynintendoparental/__init__.py**
  * Updated the `update_device` function to call the new `self._api.async_get_account_devices()` method instead of `self._api.send_request(...)` (line 33).
* **pynintendoparental/api.py**
  * Added a `session` parameter to the `__init__` method and stored it as `self._session` (line 28).
  * Modified the `send_request` method to use the stored `self._session` for making requests, removing the `async with aiohttp.ClientSession() as session:` block (line 66).
  * Added new asynchronous methods wrapping `send_request` for specific API endpoints: `async_get_account_details` (line 98), `async_get_account_devices` (line 105), `async_get_account_device` (line 112), `async_get_device_daily_summaries` (line 120), `async_get_device_monthly_summaries` (line 127), `async_get_device_parental_control_setting` (line 134), `async_get_device_parental_control_setting_state` (line 141), `async_get_device_alarm_setting_state` (line 148), `async_get_device_monthly_summary` (line 155), `async_set_device_parental_control_setting` (line 164), `async_set_device_whitelisted_applications` (line 176), and `async_set_device_alarm_setting_state` (line 188).
* **pynintendoparental/device.py**
  * Updated `_set_parental_control_setting` to call `self._api.async_set_device_parental_control_setting` (line 118).
  * Updated `_get_parental_control_setting` to call `self._api.async_get_device_parental_control_setting` (line 211).
  * Updated `_get_daily_summaries` to call `self._api.async_get_device_daily_summaries` (line 236).
  * Updated `_get_extras` to call `self._api.async_get_account_device` (line 334).
  * Updated `get_monthly_summary` (when `search_date` is None) to call `self._api.async_get_device_monthly_summaries` (line 349).
  * Updated `get_monthly_summary` (when `search_date` is provided) to call `self._api.async_get_device_monthly_summary` (line 359).
  * Updated `set_alarm_state` to call `self._api.async_set_device_alarm_setting_state` (line 381).
  * Updated `set_whitelisted_application` to call `self._api.async_set_device_whitelisted_applications` (line 398).

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> Async calls now fly,
> No session per request dies,
> Performance takes wing.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:43:25Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request effectively refactors the API calls within the `Device` class to utilize new asynchronous methods in the `Api` class, aiming to improve performance by reusing an `aiohttp.ClientSession`. The introduction of specific `async_...` methods in `Api` is a good step towards a cleaner and more maintainable API client.

However, there's a critical issue regarding the new `session` parameter required by `Api.__init__`. The primary consumer, `NintendoParental`, doesn't appear to be updated to provide this session during `Api` instantiation, which will likely lead to a `TypeError` at runtime. Additionally, there are a few areas in the `Api` class itself that could be further improved for robustness and clarity.

No explicit style guide was provided, so feedback related to style defaults to common Python best practices (e.g., PEP 8).

### Summary of Findings
* **Critical: API Initialization Error**: The `Api` class constructor now requires an `aiohttp.ClientSession` argument, but its primary caller (`NintendoParental`) has not been updated to provide it. This will cause a `TypeError` at runtime.
* **High: HTTP Header Management**: Using `session.headers.add()` repeatedly for standard headers might lead to duplicates. Prefer direct assignment (`headers[key]=value`) or `headers.update()` for clarity and to ensure single values for such headers.
* **Medium: JSON Parsing Robustness**: The code assumes a successful HTTP response (2xx) will always contain valid JSON. Adding error handling around `response.json()` would make it more resilient to unexpected server responses.
* **Medium: Type Hinting**: The new `session` parameter and attribute in `Api.__init__` should be type-hinted with `aiohttp.ClientSession` for better code clarity.

### Merge Readiness
Due to the critical issue identified with `Api` initialization (missing `session` argument from `NintendoParental`), this pull request is not ready to be merged. Addressing this issue is essential for the code to function correctly. The high and medium severity issues should also be considered for improvement.

I am unable to approve pull requests myself; please ensure these changes are reviewed and approved by a maintainer before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:45:33Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request effectively refactors API interactions by introducing specific asynchronous methods in the `Api` class and leveraging a persistent `aiohttp.ClientSession`. This is a significant improvement for performance and code clarity. The changes in the `Device` class to use these new methods are well-executed. Overall, the PR is a positive step towards a more efficient and maintainable codebase. One critical aspect regarding `aiohttp.ClientSession` lifecycle management needs to be addressed.

### Summary of Findings
* **aiohttp.ClientSession Lifecycle Management**: The `Api` class creates an `aiohttp.ClientSession` if one is not provided during initialization. This internally created session is not currently closed, which can lead to resource leaks (e.g., open network connections) in applications, especially if `Api` instances are frequently created or long-lived. It's crucial to ensure these sessions are properly closed. Suggested solutions include making `Api` an async context manager or adding an explicit `async_close()` method.

### Merge Readiness
The pull request introduces valuable performance improvements by refactoring API calls to be asynchronous and reusing `aiohttp.ClientSession`. However, there is a high-severity issue concerning the lifecycle management of the internally created `aiohttp.ClientSession` in the `Api` class, which could lead to resource leaks. I recommend addressing this issue before merging. I am not authorized to approve pull requests, so please ensure further review and approval from maintainers after addressing the feedback.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:56:53Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request significantly refactors the `Api` class to use asynchronous methods more effectively, primarily by introducing a reusable `aiohttp.ClientSession` and dedicated async methods for API endpoints. This is a great improvement for both performance and maintainability.

The key changes include:
- Managing an `aiohttp.ClientSession` within the `Api` class, allowing it to be reused across requests.
- Adding an `async_close` method to properly manage the lifecycle of the internally created session.
- Centralizing request header creation with a `_headers` property.
- Introducing specific `async_...` methods for each API endpoint, which makes the API client much cleaner and easier to use.
- Enhancing error handling in `send_request` for JSON decoding.

The consuming code in `Device` and `__init__.py` has been updated accordingly to use these new, more specific API methods.

Overall, the changes are well-implemented and address the goal of improving performance and code clarity. I have one suggestion for a minor improvement.

### Summary of Findings
* **Initialization of `_session_created_internally`**: In `pynintendoparental/api.py`, the `_session_created_internally` attribute is only set if a session is created internally. Initializing it to `False` by default would ensure the attribute always exists, making the class state more explicit and potentially simplifying checks in other methods (e.g., `async_close`). This was commented on with `medium` severity.
* **Type Hinting in `Api.__init__`**: In `pynintendoparental/api.py`, line 28, the `auth` parameter in `Api.__init__` could benefit from a type hint (`Authenticator`) for consistency with how `self._auth` is typed. This is a `low` severity observation for maintainability and was not added as a direct comment due to review settings.
* **Redundant Comment in `async_close`**: In `pynintendoparental/api.py`, line 69, the comment `# Optional: clear the session attribute` is redundant as the subsequent line `self._session = None` already performs this action. This is a `low` severity stylistic observation and was not added as a direct comment due to review settings.

### Merge Readiness
The pull request introduces valuable improvements to the API client's performance and maintainability. The refactoring to asynchronous methods and better session management is well executed. 

There is one `medium` severity suggestion regarding the initialization of an internal attribute for clarity and robustness. Addressing this point would further enhance the code quality.

I am not authorized to approve pull requests. Please ensure any outstanding feedback is addressed and that the changes are reviewed by other maintainers before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T16:02:32Z

Review comments:
diff_hunk: @@ -25,11 +25,12 @@ def _check_http_success(status: int) -> bool:
 class Api:
     """Nintendo Parental Controls API."""
 
-    def __init__(self, auth, tz, lang):
+    def __init__(self, auth, tz, lang, session):
         """INIT"""
         self._auth: Authenticator = auth
         self._tz = tz
         self._language = lang
+        self._session = session
path: pynintendoparental/api.py
user: login: gemini-code-assist[bot]
body: ![critical](https://img.shields.io/badge/priority-critical-red)

The `__init__` method now correctly accepts a `session` parameter, which is essential for reusing the `aiohttp.ClientSession`. This is a good change for performance.

1.  **Critical Issue with Callers**: The `session` parameter is now mandatory. However, the primary instantiation of `Api` (e.g., within `NintendoParental.__init__` in `pynintendoparental/__init__.py`) doesn't seem to be updated to pass this `session` argument. This will likely cause a `TypeError` when `NintendoParental` (and thus `Api`) is initialized. Could you please ensure that all callers of `Api()` are updated to provide the required `aiohttp.ClientSession`? For instance, `NintendoParental` might need to accept a session in its constructor and/or `create` method, which would then be passed to the `Api` constructor.

2.  **Type Hinting**: To improve code clarity and maintainability, it would be beneficial to add type hints for the `session` parameter and the `self._session` attribute. It should be `aiohttp.ClientSession`.

For point 2, here's a suggestion:

```suggestion
    def __init__(self, auth, tz, lang, session: aiohttp.ClientSession):
        """INIT"""
        self._auth: Authenticator = auth
        self._tz = tz
        self._language = lang
        self._session: aiohttp.ClientSession = session
```
created_at: 2025-05-21T15:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,35 +63,136 @@
             "json": "",
             "headers": ""
         }
-        async with aiohttp.ClientSession() as session:
-            # Add auth header
-            session.headers.add("Authorization", self._auth_token)
-            session.headers.add("User-Agent", USER_AGENT)
-            session.headers.add("X-Moon-App-Id", MOBILE_APP_PKG)
-            session.headers.add("X-Moon-Os", OS_NAME)
-            session.headers.add("X-Moon-Os-Version", OS_VERSION)
-            session.headers.add("X-Moon-Model", DEVICE_MODEL)
-            session.headers.add("X-Moon-TimeZone", self._tz)
-            session.headers.add("X-Moon-Os-Language", self._language)
-            session.headers.add("X-Moon-App-Language", self._language)
-            session.headers.add("X-Moon-App-Display-Version", MOBILE_APP_VERSION)
-            session.headers.add("X-Moon-App-Internal-Version", MOBILE_APP_BUILD)
-            async with session.request(
-                method=e_point.get("method"),
-                url=url,
-                json=body
-            ) as response:
-                _LOGGER.debug("%s request to %s status code %s",
-                              e_point.get("method"),
-                              url,
-                              response.status)
-                if _check_http_success(response.status):
-                    resp["status"] = response.status
-                    resp["text"] = await response.text()
-                    resp["json"] = await response.json()
-                    resp["headers"] = response.headers
-                else:
-                    raise HttpException("HTTP Error", response.status, await response.text())
+        # Add auth header
+        self._session.headers.add("Authorization", self._auth_token)
+        self._session.headers.add("User-Agent", USER_AGENT)
+        self._session.headers.add("X-Moon-App-Id", MOBILE_APP_PKG)
+        self._session.headers.add("X-Moon-Os", OS_NAME)
+        self._session.headers.add("X-Moon-Os-Version", OS_VERSION)
+        self._session.headers.add("X-Moon-Model", DEVICE_MODEL)
+        self._session.headers.add("X-Moon-TimeZone", self._tz)
+        self._session.headers.add("X-Moon-Os-Language", self._language)
+        self._session.headers.add("X-Moon-App-Language", self._language)
+        self._session.headers.add("X-Moon-App-Display-Version", MOBILE_APP_VERSION)
+        self._session.headers.add("X-Moon-App-Internal-Version", MOBILE_APP_BUILD)
path: pynintendoparental/api.py
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

It's good that the headers are being set on the shared session. However, using `self._session.headers.add(...)` repeatedly for standard HTTP headers might lead to unintended behavior if not handled carefully by `aiohttp` (specifically, `CIMultiDict.add` allows duplicate headers with the same name).

For headers that should typically have a single, authoritative value (like `Authorization`, `User-Agent`, etc.), it's generally safer and clearer to assign them directly using dictionary-style assignment (e.g., `self._session.headers['Header-Name'] = 'value'`) or by using `self._session.headers.update({...})` once with all necessary headers. This ensures that any previous values for these headers are replaced rather than potentially duplicated.

Could you consider changing this block to use direct assignment or `update` to prevent potential issues with duplicate headers and to more clearly express the intent of setting/overwriting these header values?

For example:
```python
# Current approach:
# self._session.headers.add("Authorization", self._auth_token)
# self._session.headers.add("User-Agent", USER_AGENT)
# ...

# Suggested approach (example using update):
# headers_to_set = {
#     "Authorization": self._auth_token,
#     "User-Agent": USER_AGENT,
#     "X-Moon-App-Id": MOBILE_APP_PKG,
#     # ... other headers
# }
# self._session.headers.update(headers_to_set)

# Or individual assignments:
# self._session.headers["Authorization"] = self._auth_token
# self._session.headers["User-Agent"] = USER_AGENT
# # etc.
```
created_at: 2025-05-21T15:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,35 +63,136 @@
             "json": "",
             "headers": ""
         }
-        async with aiohttp.ClientSession() as session:
-            # Add auth header
-            session.headers.add("Authorization", self._auth_token)
-            session.headers.add("User-Agent", USER_AGENT)
-            session.headers.add("X-Moon-App-Id", MOBILE_APP_PKG)
-            session.headers.add("X-Moon-Os", OS_NAME)
-            session.headers.add("X-Moon-Os-Version", OS_VERSION)
-            session.headers.add("X-Moon-Model", DEVICE_MODEL)
-            session.headers.add("X-Moon-TimeZone", self._tz)
-            session.headers.add("X-Moon-Os-Language", self._language)
-            session.headers.add("X-Moon-App-Language", self._language)
-            session.headers.add("X-Moon-App-Display-Version", MOBILE_APP_VERSION)
-            session.headers.add("X-Moon-App-Internal-Version", MOBILE_APP_BUILD)
-            async with session.request(
-                method=e_point.get("method"),
-                url=url,
-                json=body
-            ) as response:
-                _LOGGER.debug("%s request to %s status code %s",
-                              e_point.get("method"),
-                              url,
-                              response.status)
-                if _check_http_success(response.status):
-                    resp["status"] = response.status
-                    resp["text"] = await response.text()
-                    resp["json"] = await response.json()
-                    resp["headers"] = response.headers
-                else:
-                    raise HttpException("HTTP Error", response.status, await response.text())
+        # Add auth header
+        self._session.headers.add("Authorization", self._auth_token)
+        self._session.headers.add("User-Agent", USER_AGENT)
+        self._session.headers.add("X-Moon-App-Id", MOBILE_APP_PKG)
+        self._session.headers.add("X-Moon-Os", OS_NAME)
+        self._session.headers.add("X-Moon-Os-Version", OS_VERSION)
+        self._session.headers.add("X-Moon-Model", DEVICE_MODEL)
+        self._session.headers.add("X-Moon-TimeZone", self._tz)
+        self._session.headers.add("X-Moon-Os-Language", self._language)
+        self._session.headers.add("X-Moon-App-Language", self._language)
+        self._session.headers.add("X-Moon-App-Display-Version", MOBILE_APP_VERSION)
+        self._session.headers.add("X-Moon-App-Internal-Version", MOBILE_APP_BUILD)
+        async with self._session.request(
+            method=e_point.get("method"),
+            url=url,
+            json=body
+        ) as response:
+            _LOGGER.debug("%s request to %s status code %s",
+                            e_point.get("method"),
+                            url,
+                            response.status)
+            if _check_http_success(response.status):
+                resp["status"] = response.status
+                resp["text"] = await response.text()
+                resp["json"] = await response.json()
path: pynintendoparental/api.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The line `resp["json"] = await response.json()` assumes the response will always be valid JSON if the HTTP status is successful. However, there might be edge cases (e.g., unexpected server behavior, empty response body despite 2xx, proxy interference) where a 2xx response doesn't contain valid JSON. This could lead to an `aiohttp.ContentTypeError` or `json.JSONDecodeError` (which `aiohttp` might raise as `ValueError` or a more specific client exception).

To make this more robust, would it be worth wrapping the `await response.json()` call in a `try...except` block to handle potential parsing errors gracefully? You could log the error and perhaps set `resp["json"]` to a default value (like `None` or an empty dict) or re-raise a custom library-specific exception.

```python
                    try:
                        resp["json"] = await response.json()
                    except (aiohttp.ContentTypeError, ValueError) as e: # ValueError can be raised for JSONDecodeError
                        _LOGGER.warning(f"Failed to decode JSON response from {url}. Status: {response.status}, Error: {e}. Response text (first 200 chars): {resp['text'][:200]}...")
                        resp["json"] = {} # Or None, or re-raise a more specific library error
```
created_at: 2025-05-21T15:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,11 +25,14 @@ def _check_http_success(status: int) -> bool:
 class Api:
     """Nintendo Parental Controls API."""
 
-    def __init__(self, auth, tz, lang):
+    def __init__(self, auth, tz, lang, session: aiohttp.ClientSession=None):
         """INIT"""
         self._auth: Authenticator = auth
         self._tz = tz
         self._language = lang
+        if session is None:
+            session = aiohttp.ClientSession()
+        self._session = session
path: pynintendoparental/api.py
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `Api` class now correctly initializes an `aiohttp.ClientSession` if one isn't provided. However, this internally created session needs to be closed when the `Api` instance is no longer in use to prevent resource leaks (e.g., unclosed connections, file descriptors).

Could you consider implementing a way to close this session? Common patterns include:
1.  Making the `Api` class an asynchronous context manager (by implementing `__aenter__` and `__aexit__`). In `__aexit__`, you would close the session if it was created internally.
2.  Adding an explicit `async_close()` method to the `Api` class that users must call.

This is important for the long-term stability and resource management of applications using this library. For example:

```python
# In Api class

# Add a flag in __init__ to track if the session was created internally
# self._session_created_internally = False
# if session is None:
#     session = aiohttp.ClientSession()
#     self._session_created_internally = True
# self._session = session

async def async_close(self):
    """Closes the underlying aiohttp.ClientSession if it was created by this instance."""
    if hasattr(self, '_session_created_internally') and self._session_created_internally and self._session and not self._session.closed:
        await self._session.close()
        self._session = None # Optional: clear the session attribute
```

The calling code (e.g., `NintendoParental` class or the end-user application) would then be responsible for calling `await api_instance.async_close()` at the appropriate time.
created_at: 2025-05-21T15:56:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,11 +25,15 @@ def _check_http_success(status: int) -> bool:
 class Api:
     """Nintendo Parental Controls API."""
 
-    def __init__(self, auth, tz, lang):
+    def __init__(self, auth, tz, lang, session: aiohttp.ClientSession=None):
         """INIT"""
         self._auth: Authenticator = auth
         self._tz = tz
         self._language = lang
+        if session is None:
+            session = aiohttp.ClientSession()
+            self._session_created_internally = True
+        self._session = session
path: pynintendoparental/api.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider initializing `_session_created_internally` to `False` at the start of this block or earlier in the `__init__` method. This ensures the attribute always exists on the instance, regardless of whether a session is passed in or created internally. 

Benefits:
*   Makes the class's state more explicit and predictable.
*   Could simplify logic in other methods like `async_close` by removing the need for `hasattr(self, '_session_created_internally')` if the attribute is guaranteed to be initialized.

What are your thoughts on this adjustment for improved explicitness?

```suggestion
        self._session_created_internally = False
        if session is None:
            session = aiohttp.ClientSession()
            self._session_created_internally = True
        self._session = session
```
created_at: 2025-05-21T16:02:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-21T15:43:16Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>Scanned Files</h2>
None

<!-- dependency-review-pr-comment-marker -->
author_association: CONTRIBUTOR
user: login: pantherale0
created_at: 2025-05-21T16:00:59Z
body: /gemini review
author_association: OWNER