pine-assistant 0.3.0__tar.gz → 0.3.2__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.
- {pine_assistant-0.3.0/src/pine_assistant.egg-info → pine_assistant-0.3.2}/PKG-INFO +2 -2
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/pyproject.toml +2 -2
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/__init__.py +1 -1
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/client.py +24 -3
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/transport/socketio.py +51 -8
- {pine_assistant-0.3.0 → pine_assistant-0.3.2/src/pine_assistant.egg-info}/PKG-INFO +2 -2
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant.egg-info/SOURCES.txt +3 -1
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant.egg-info/requires.txt +1 -1
- pine_assistant-0.3.2/tests/test_device_id.py +57 -0
- pine_assistant-0.3.2/tests/test_socketio_connect.py +149 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/LICENSE +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/README.md +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/setup.cfg +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/auth.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/chat.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/cli/__init__.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/cli/auth.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/cli/chat.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/cli/main.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/cli/sessions.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/cli/tasks.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/errors.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/__init__.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/envelope.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/events.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/form.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/payment.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/session.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/models/task.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/py.typed +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/sessions.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/transport/__init__.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/transport/envelope.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant/transport/http.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant.egg-info/dependency_links.txt +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant.egg-info/entry_points.txt +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant.egg-info/top_level.txt +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/tests/test_basics.py +0 -0
- {pine_assistant-0.3.0 → pine_assistant-0.3.2}/tests/test_chat_engine.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pine-assistant
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Pine AI SDK — Let Pine AI handle your digital chores. Socket.IO + REST client.
|
|
5
5
|
Author: Pine AI
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: python-socketio
|
|
23
|
+
Requires-Dist: python-socketio>=5.11.0
|
|
24
24
|
Requires-Dist: httpx>=0.27.0
|
|
25
25
|
Requires-Dist: pydantic>=2.0.0
|
|
26
26
|
Provides-Extra: cli
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pine-assistant"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Pine AI SDK — Let Pine AI handle your digital chores. Socket.IO + REST client."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"python-socketio
|
|
26
|
+
"python-socketio>=5.11.0",
|
|
27
27
|
"httpx>=0.27.0",
|
|
28
28
|
"pydantic>=2.0.0",
|
|
29
29
|
]
|
|
@@ -11,7 +11,7 @@ from pine_assistant.sessions import SessionsAPI
|
|
|
11
11
|
from pine_assistant.errors import PineAIError, AuthError, SessionError, ConnectionError
|
|
12
12
|
from pine_assistant.models.events import C2SEvent, S2CEvent, NotificationEvent
|
|
13
13
|
|
|
14
|
-
__version__ = "0.3.
|
|
14
|
+
__version__ = "0.3.2"
|
|
15
15
|
__all__ = [
|
|
16
16
|
"PineAI",
|
|
17
17
|
"AsyncPineAI",
|
|
@@ -3,6 +3,8 @@ PineAI / AsyncPineAI — main SDK clients.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
6
8
|
import uuid
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import Any, AsyncGenerator, Generator, Optional
|
|
@@ -16,20 +18,39 @@ from pine_assistant.errors import ConnectionError
|
|
|
16
18
|
from pine_assistant.models.events import C2SEvent
|
|
17
19
|
|
|
18
20
|
DEVICE_ID_FILE = Path.home() / ".pine" / "device_id"
|
|
21
|
+
DEVICE_ID_ENV = "PINE_DEVICE_ID"
|
|
22
|
+
|
|
23
|
+
_logger = logging.getLogger(__name__)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def _get_or_create_device_id(provided: Optional[str] = None) -> str:
|
|
27
|
+
"""Resolve a stable device_id.
|
|
28
|
+
|
|
29
|
+
Precedence: explicit argument → PINE_DEVICE_ID env var → ~/.pine/device_id
|
|
30
|
+
→ a new random UUID persisted to ~/.pine/device_id.
|
|
31
|
+
|
|
32
|
+
If persistence fails, a warning is logged so callers running in sandboxed
|
|
33
|
+
or read-only environments learn why their device_id rotates on each launch
|
|
34
|
+
— set PINE_DEVICE_ID to pin it.
|
|
35
|
+
"""
|
|
22
36
|
if provided:
|
|
23
37
|
return provided
|
|
38
|
+
env_value = os.environ.get(DEVICE_ID_ENV)
|
|
39
|
+
if env_value:
|
|
40
|
+
return env_value.strip()
|
|
24
41
|
try:
|
|
25
42
|
return DEVICE_ID_FILE.read_text().strip()
|
|
26
|
-
except FileNotFoundError:
|
|
43
|
+
except (FileNotFoundError, OSError):
|
|
27
44
|
device_id = str(uuid.uuid4())
|
|
28
45
|
try:
|
|
29
46
|
DEVICE_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
30
47
|
DEVICE_ID_FILE.write_text(device_id)
|
|
31
|
-
except OSError:
|
|
32
|
-
|
|
48
|
+
except OSError as exc:
|
|
49
|
+
_logger.warning(
|
|
50
|
+
"Could not persist device_id to %s (%s). "
|
|
51
|
+
"Set the %s env var to pin a stable device_id.",
|
|
52
|
+
DEVICE_ID_FILE, exc, DEVICE_ID_ENV,
|
|
53
|
+
)
|
|
33
54
|
return device_id
|
|
34
55
|
|
|
35
56
|
|
|
@@ -11,9 +11,24 @@ from typing import Any, Callable, Optional
|
|
|
11
11
|
|
|
12
12
|
import socketio
|
|
13
13
|
|
|
14
|
+
from pine_assistant.errors import ConnectionError as PineConnectionError
|
|
15
|
+
|
|
14
16
|
SOCKETIO_PATH = "/api/v2/socket.io/"
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def _format_connect_error(data: Any) -> str:
|
|
20
|
+
"""Render a server-supplied connect_error payload into a human-readable string."""
|
|
21
|
+
if data is None:
|
|
22
|
+
return "no payload"
|
|
23
|
+
if isinstance(data, str):
|
|
24
|
+
return data
|
|
25
|
+
if isinstance(data, dict):
|
|
26
|
+
msg = data.get("message") or data.get("error") or data.get("reason")
|
|
27
|
+
if msg:
|
|
28
|
+
return str(msg)
|
|
29
|
+
return repr(data)
|
|
30
|
+
|
|
31
|
+
|
|
17
32
|
class SocketIOManager:
|
|
18
33
|
def __init__(
|
|
19
34
|
self,
|
|
@@ -66,11 +81,16 @@ class SocketIOManager:
|
|
|
66
81
|
|
|
67
82
|
self._sio = socketio.AsyncClient()
|
|
68
83
|
ready_event = asyncio.Event()
|
|
84
|
+
connect_error_data: dict[str, Any] = {}
|
|
69
85
|
|
|
70
86
|
@self._sio.event
|
|
71
87
|
async def connect() -> None:
|
|
72
88
|
pass
|
|
73
89
|
|
|
90
|
+
@self._sio.event
|
|
91
|
+
async def connect_error(data: Any) -> None:
|
|
92
|
+
connect_error_data["payload"] = data
|
|
93
|
+
|
|
74
94
|
@self._sio.on("ready")
|
|
75
95
|
async def on_ready(*_args: Any) -> None:
|
|
76
96
|
self._connected = True
|
|
@@ -93,19 +113,36 @@ class SocketIOManager:
|
|
|
93
113
|
async def disconnect(_reason: str = "") -> None:
|
|
94
114
|
self._connected = False
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
try:
|
|
117
|
+
await self._sio.connect(
|
|
118
|
+
self._base_url,
|
|
119
|
+
auth={"token": self._token},
|
|
120
|
+
transports=self._transports,
|
|
121
|
+
socketio_path=SOCKETIO_PATH,
|
|
122
|
+
wait_timeout=self._ready_timeout,
|
|
123
|
+
)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
# python-socketio raises the generic "One or more namespaces
|
|
126
|
+
# failed to connect" when the server replies with connect_error.
|
|
127
|
+
# Surface the actual server-supplied reason if we captured one.
|
|
128
|
+
if "payload" in connect_error_data:
|
|
129
|
+
reason = _format_connect_error(connect_error_data["payload"])
|
|
130
|
+
raise PineConnectionError(
|
|
131
|
+
f"Socket.IO connect rejected by server: {reason}"
|
|
132
|
+
) from exc
|
|
133
|
+
raise PineConnectionError(f"Socket.IO connect failed: {exc}") from exc
|
|
103
134
|
|
|
104
135
|
try:
|
|
105
136
|
await asyncio.wait_for(ready_event.wait(), timeout=self._ready_timeout)
|
|
106
137
|
except asyncio.TimeoutError:
|
|
107
138
|
await self._sio.disconnect()
|
|
108
|
-
|
|
139
|
+
# The Pine backend accepts the WebSocket but only emits 'ready' after
|
|
140
|
+
# its own auth check. A timeout here almost always means the token or
|
|
141
|
+
# user_id is rejected — surface that hint instead of a generic timeout.
|
|
142
|
+
raise PineConnectionError(
|
|
143
|
+
f"Socket.IO connected but no 'ready' event after {self._ready_timeout}s. "
|
|
144
|
+
"This usually means access_token or user_id is invalid/expired — re-run the auth flow."
|
|
145
|
+
)
|
|
109
146
|
|
|
110
147
|
def emit(
|
|
111
148
|
self,
|
|
@@ -136,6 +173,12 @@ class SocketIOManager:
|
|
|
136
173
|
)
|
|
137
174
|
|
|
138
175
|
async def _do_emit() -> None:
|
|
176
|
+
# The socket may be torn down between scheduling and execution —
|
|
177
|
+
# e.g. user calls leave_session() then disconnect() back-to-back.
|
|
178
|
+
# In that case the emit can't possibly succeed; skip silently
|
|
179
|
+
# instead of logging a misleading "/ is not a connected namespace".
|
|
180
|
+
if not self._sio or not self._sio.connected:
|
|
181
|
+
return
|
|
139
182
|
try:
|
|
140
183
|
await self._sio.emit(event_type, envelope) # type: ignore[union-attr]
|
|
141
184
|
except Exception as e:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pine-assistant
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Pine AI SDK — Let Pine AI handle your digital chores. Socket.IO + REST client.
|
|
5
5
|
Author: Pine AI
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: python-socketio
|
|
23
|
+
Requires-Dist: python-socketio>=5.11.0
|
|
24
24
|
Requires-Dist: httpx>=0.27.0
|
|
25
25
|
Requires-Dist: pydantic>=2.0.0
|
|
26
26
|
Provides-Extra: cli
|
|
@@ -32,4 +32,6 @@ src/pine_assistant/transport/envelope.py
|
|
|
32
32
|
src/pine_assistant/transport/http.py
|
|
33
33
|
src/pine_assistant/transport/socketio.py
|
|
34
34
|
tests/test_basics.py
|
|
35
|
-
tests/test_chat_engine.py
|
|
35
|
+
tests/test_chat_engine.py
|
|
36
|
+
tests/test_device_id.py
|
|
37
|
+
tests/test_socketio_connect.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Unit tests for device_id resolution — env var, explicit arg, file persistence."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pine_assistant.client as client_module
|
|
7
|
+
from pine_assistant.client import _get_or_create_device_id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_explicit_arg_wins(monkeypatch, tmp_path):
|
|
11
|
+
monkeypatch.setenv("PINE_DEVICE_ID", "from-env")
|
|
12
|
+
monkeypatch.setattr(client_module, "DEVICE_ID_FILE", tmp_path / "device_id")
|
|
13
|
+
assert _get_or_create_device_id("explicit") == "explicit"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_env_var_wins_over_file(monkeypatch, tmp_path):
|
|
17
|
+
device_file = tmp_path / "device_id"
|
|
18
|
+
device_file.write_text("from-file")
|
|
19
|
+
monkeypatch.setenv("PINE_DEVICE_ID", "from-env")
|
|
20
|
+
monkeypatch.setattr(client_module, "DEVICE_ID_FILE", device_file)
|
|
21
|
+
assert _get_or_create_device_id() == "from-env"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_env_var_is_stripped(monkeypatch, tmp_path):
|
|
25
|
+
monkeypatch.setenv("PINE_DEVICE_ID", " trimmed \n")
|
|
26
|
+
monkeypatch.setattr(client_module, "DEVICE_ID_FILE", tmp_path / "device_id")
|
|
27
|
+
assert _get_or_create_device_id() == "trimmed"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_falls_back_to_file(monkeypatch, tmp_path):
|
|
31
|
+
monkeypatch.delenv("PINE_DEVICE_ID", raising=False)
|
|
32
|
+
device_file = tmp_path / "device_id"
|
|
33
|
+
device_file.write_text("from-file")
|
|
34
|
+
monkeypatch.setattr(client_module, "DEVICE_ID_FILE", device_file)
|
|
35
|
+
assert _get_or_create_device_id() == "from-file"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_creates_and_persists_when_missing(monkeypatch, tmp_path):
|
|
39
|
+
monkeypatch.delenv("PINE_DEVICE_ID", raising=False)
|
|
40
|
+
device_file = tmp_path / "nested" / "device_id"
|
|
41
|
+
monkeypatch.setattr(client_module, "DEVICE_ID_FILE", device_file)
|
|
42
|
+
first = _get_or_create_device_id()
|
|
43
|
+
assert device_file.read_text() == first
|
|
44
|
+
# A second call returns the persisted value, not a new UUID.
|
|
45
|
+
assert _get_or_create_device_id() == first
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_unwritable_dir_warns_and_returns_random(monkeypatch, tmp_path, caplog):
|
|
49
|
+
monkeypatch.delenv("PINE_DEVICE_ID", raising=False)
|
|
50
|
+
# Point at an unwritable location: a file used as a "directory".
|
|
51
|
+
blocker = tmp_path / "blocker"
|
|
52
|
+
blocker.write_text("not a directory")
|
|
53
|
+
monkeypatch.setattr(client_module, "DEVICE_ID_FILE", blocker / "device_id")
|
|
54
|
+
with caplog.at_level("WARNING", logger="pine_assistant.client"):
|
|
55
|
+
device_id = _get_or_create_device_id()
|
|
56
|
+
assert device_id # got *some* id back
|
|
57
|
+
assert any("PINE_DEVICE_ID" in rec.message for rec in caplog.records)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Unit tests for SocketIOManager.connect() — connect_error surfacing."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from pine_assistant.errors import ConnectionError as PineConnectionError
|
|
10
|
+
from pine_assistant.transport.socketio import (
|
|
11
|
+
SocketIOManager,
|
|
12
|
+
_format_connect_error,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_format_connect_error_string():
|
|
17
|
+
assert _format_connect_error("bad token") == "bad token"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_format_connect_error_dict_message():
|
|
21
|
+
assert _format_connect_error({"message": "invalid token"}) == "invalid token"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_format_connect_error_dict_error_key():
|
|
25
|
+
assert _format_connect_error({"error": "unauthorized"}) == "unauthorized"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_format_connect_error_none():
|
|
29
|
+
assert _format_connect_error(None) == "no payload"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_format_connect_error_unknown():
|
|
33
|
+
assert _format_connect_error([1, 2]) == "[1, 2]"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _FakeAsyncClient:
|
|
37
|
+
"""Minimal stand-in for socketio.AsyncClient that lets a test drive the
|
|
38
|
+
connect_error path."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, connect_error_payload: Any = None) -> None:
|
|
41
|
+
self._handlers: dict[str, Any] = {}
|
|
42
|
+
self._connect_error_payload = connect_error_payload
|
|
43
|
+
self.connected = False
|
|
44
|
+
|
|
45
|
+
def event(self, fn):
|
|
46
|
+
self._handlers[fn.__name__] = fn
|
|
47
|
+
return fn
|
|
48
|
+
|
|
49
|
+
def on(self, name):
|
|
50
|
+
def deco(fn):
|
|
51
|
+
self._handlers[name] = fn
|
|
52
|
+
return fn
|
|
53
|
+
return deco
|
|
54
|
+
|
|
55
|
+
async def connect(self, *_args, **_kwargs):
|
|
56
|
+
# Simulate the server replying with connect_error, then python-socketio
|
|
57
|
+
# raising the generic ConnectionError after wait_timeout.
|
|
58
|
+
handler = self._handlers.get("connect_error")
|
|
59
|
+
if handler is not None:
|
|
60
|
+
await handler(self._connect_error_payload)
|
|
61
|
+
raise ConnectionError("One or more namespaces failed to connect")
|
|
62
|
+
|
|
63
|
+
async def disconnect(self):
|
|
64
|
+
self.connected = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_connect_error_payload_surfaces_in_pine_connection_error():
|
|
69
|
+
fake = _FakeAsyncClient(connect_error_payload={"message": "invalid token"})
|
|
70
|
+
mgr = SocketIOManager(
|
|
71
|
+
base_url="https://example.test",
|
|
72
|
+
token="bad-token",
|
|
73
|
+
user_id="u1",
|
|
74
|
+
device_id="d1",
|
|
75
|
+
ready_timeout=0.1,
|
|
76
|
+
)
|
|
77
|
+
with patch("pine_assistant.transport.socketio.socketio.AsyncClient", return_value=fake):
|
|
78
|
+
with pytest.raises(PineConnectionError) as excinfo:
|
|
79
|
+
await mgr.connect()
|
|
80
|
+
msg = str(excinfo.value)
|
|
81
|
+
assert "rejected by server" in msg
|
|
82
|
+
assert "invalid token" in msg
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_connect_error_without_payload_falls_back_to_generic():
|
|
87
|
+
fake = _FakeAsyncClient(connect_error_payload=None)
|
|
88
|
+
# Override: don't invoke the connect_error handler at all — simulate a
|
|
89
|
+
# transport-level failure where the server never sent connect_error.
|
|
90
|
+
async def connect_no_event(*_args, **_kwargs):
|
|
91
|
+
raise ConnectionError("transport closed")
|
|
92
|
+
fake.connect = connect_no_event # type: ignore[method-assign]
|
|
93
|
+
|
|
94
|
+
mgr = SocketIOManager(
|
|
95
|
+
base_url="https://example.test",
|
|
96
|
+
token="t",
|
|
97
|
+
user_id="u",
|
|
98
|
+
device_id="d",
|
|
99
|
+
ready_timeout=0.1,
|
|
100
|
+
)
|
|
101
|
+
with patch("pine_assistant.transport.socketio.socketio.AsyncClient", return_value=fake):
|
|
102
|
+
with pytest.raises(PineConnectionError) as excinfo:
|
|
103
|
+
await mgr.connect()
|
|
104
|
+
assert "transport closed" in str(excinfo.value)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _SilentStallClient:
|
|
108
|
+
"""Stand-in that accepts the connect but never fires the 'ready' event —
|
|
109
|
+
matches Pine backend's behavior when access_token/user_id is rejected."""
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
self._handlers: dict[str, Any] = {}
|
|
113
|
+
self.connected = False
|
|
114
|
+
|
|
115
|
+
def event(self, fn):
|
|
116
|
+
self._handlers[fn.__name__] = fn
|
|
117
|
+
return fn
|
|
118
|
+
|
|
119
|
+
def on(self, name):
|
|
120
|
+
def deco(fn):
|
|
121
|
+
self._handlers[name] = fn
|
|
122
|
+
return fn
|
|
123
|
+
return deco
|
|
124
|
+
|
|
125
|
+
async def connect(self, *_args, **_kwargs):
|
|
126
|
+
self.connected = True
|
|
127
|
+
|
|
128
|
+
async def disconnect(self):
|
|
129
|
+
self.connected = False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.asyncio
|
|
133
|
+
async def test_silent_stall_raises_diagnostic_pine_error():
|
|
134
|
+
"""When the WS connects but no 'ready' event arrives, surface a hint that
|
|
135
|
+
points the user at re-auth, not a generic asyncio TimeoutError."""
|
|
136
|
+
fake = _SilentStallClient()
|
|
137
|
+
mgr = SocketIOManager(
|
|
138
|
+
base_url="https://example.test",
|
|
139
|
+
token="t",
|
|
140
|
+
user_id="u",
|
|
141
|
+
device_id="d",
|
|
142
|
+
ready_timeout=0.1,
|
|
143
|
+
)
|
|
144
|
+
with patch("pine_assistant.transport.socketio.socketio.AsyncClient", return_value=fake):
|
|
145
|
+
with pytest.raises(PineConnectionError) as excinfo:
|
|
146
|
+
await mgr.connect()
|
|
147
|
+
msg = str(excinfo.value)
|
|
148
|
+
assert "no 'ready' event" in msg
|
|
149
|
+
assert "re-run the auth flow" in msg
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pine_assistant-0.3.0 → pine_assistant-0.3.2}/src/pine_assistant.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|