Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions growattServer/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,24 @@ class GrowattApi:
server_url = "https://openapi.growatt.com/"
agent_identifier = "Dalvik/2.1.0 (Linux; U; Android 12; https://github.com/indykoning/PyPi_GrowattServer)"

def __init__(self, add_random_user_id: bool = False, agent_identifier: str | None = None) -> None:
def __init__(
self,
add_random_user_id: bool = False,
agent_identifier: str | None = None,
session: requests.Session | None = None,
) -> None:
"""
Initialize the Growatt API client.

Args:
add_random_user_id: Append a short random suffix to the user-agent.
agent_identifier: Optional override for the user-agent string.
session: An existing :class:`requests.Session` to use instead of
creating a fresh one. Passing one lets a consumer share a
single session across API instances and restore persisted
cookies at startup, so a login is not needed on every call --
which is what drives accounts into the 507 lockout. The library
stores nothing itself: persistence stays the caller's business.

"""
if agent_identifier is not None:
Expand All @@ -69,14 +80,19 @@ def __init__(self, add_random_user_id: bool = False, agent_identifier: str | Non
random_number = "".join(str(secrets.randbelow(10)) for _ in range(5))
self.agent_identifier += " - " + random_number

self.session = requests.Session()
self.session = session if session is not None else requests.Session()

def _raise_for_status(response, *args: object, **kwargs: object) -> None:
_ = args
_ = kwargs
response.raise_for_status()

self.session.hooks = {"response": [_raise_for_status]}
# Append rather than assign: a caller-supplied session may already carry
# hooks of its own, and replacing the dict would silently drop them.
hooks = self.session.hooks.setdefault("response", [])
if callable(hooks): # requests allows a bare callable here
hooks = [hooks]
self.session.hooks["response"] = [*hooks, _raise_for_status]

headers = {"User-Agent": self.agent_identifier}
self.session.headers.update(headers)
Expand Down
75 changes: 75 additions & 0 deletions tests/test_session_injection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""A caller-supplied session lets cookies survive across API instances."""
import requests

from growattServer import GrowattApi


def test_default_still_creates_its_own_session():
api = GrowattApi()

assert isinstance(api.session, requests.Session)


def test_injected_session_is_used_as_is():
shared = requests.Session()

api = GrowattApi(session=shared)

assert api.session is shared


def test_two_instances_share_one_session():
"""The point of the parameter: one login, many API objects."""
shared = requests.Session()

first = GrowattApi(session=shared)
second = GrowattApi(session=shared)

assert first.session is second.session


def test_cookies_on_the_injected_session_survive():
"""Restoring persisted cookies at startup is the reason this exists."""
shared = requests.Session()
shared.cookies.set("JSESSIONID", "restored-from-storage")

api = GrowattApi(session=shared)

assert api.session.cookies.get("JSESSIONID") == "restored-from-storage"


def test_existing_hooks_on_the_injected_session_are_kept():
"""A shared session may already carry hooks; they must not be dropped."""
called = []

def _mine(response, *args, **kwargs):
called.append(response)

shared = requests.Session()
shared.hooks["response"] = [_mine]

api = GrowattApi(session=shared)

assert _mine in api.session.hooks["response"]
assert len(api.session.hooks["response"]) == 2


def test_bare_callable_hook_is_normalised():
"""requests permits a single callable instead of a list."""
def _mine(response, *args, **kwargs):
pass

shared = requests.Session()
shared.hooks["response"] = _mine

api = GrowattApi(session=shared)

assert _mine in api.session.hooks["response"]


def test_user_agent_is_applied_to_the_injected_session():
shared = requests.Session()

api = GrowattApi(session=shared, agent_identifier="my-agent")

assert shared.headers["User-Agent"] == "my-agent"